Repository: bumptech/glide Branch: master Commit: d4278e03c082 Files: 952 Total size: 4.0 MB Directory structure: gitextract_c7iyc7lj/ ├── .github/ │ ├── stale.yml │ └── workflows/ │ ├── build.yml │ └── publish-manual.yml ├── .gitignore ├── .gitmodules ├── .idea/ │ ├── codeStyleSettings.xml │ └── inspectionProfiles/ │ └── Project_Default.xml ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── annotation/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── compiler/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── proguard.pro │ │ ├── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── bumptech/ │ │ │ │ └── glide/ │ │ │ │ └── annotation/ │ │ │ │ └── compiler/ │ │ │ │ ├── AppModuleGenerator.java │ │ │ │ ├── AppModuleProcessor.java │ │ │ │ ├── ExtensionProcessor.java │ │ │ │ ├── GlideAnnotationProcessor.java │ │ │ │ ├── GlideExtensionValidator.java │ │ │ │ ├── GlideGenerator.java │ │ │ │ ├── IndexerGenerator.java │ │ │ │ ├── LibraryModuleProcessor.java │ │ │ │ ├── ProcessorUtil.java │ │ │ │ ├── RequestBuilderGenerator.java │ │ │ │ ├── RequestManagerFactoryGenerator.java │ │ │ │ ├── RequestManagerGenerator.java │ │ │ │ ├── RequestOptionsExtensionGenerator.java │ │ │ │ ├── RequestOptionsGenerator.java │ │ │ │ └── RequestOptionsOverrideGenerator.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── gradle/ │ │ │ └── incremental.annotation.processors │ │ └── test/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── annotation/ │ │ │ └── compiler/ │ │ │ ├── AppGlideModuleWithExcludesTest.java │ │ │ ├── AppGlideModuleWithLibraryInPackageTest.java │ │ │ ├── AppGlideModuleWithMultipleExcludesTest.java │ │ │ ├── EmptyAppAndLibraryGlideModulesTest.java │ │ │ ├── EmptyAppGlideModuleTest.java │ │ │ ├── EmptyLibraryGlideModuleTest.java │ │ │ ├── GlideExtensionOptionsTest.java │ │ │ ├── GlideExtensionWithOptionTest.java │ │ │ ├── GlideExtensionWithTypeTest.java │ │ │ ├── InvalidAppGlideModuleWithExcludesTest.java │ │ │ ├── InvalidGlideExtensionTest.java │ │ │ ├── InvalidGlideOptionsExtensionTest.java │ │ │ ├── InvalidGlideTypeExtensionTest.java │ │ │ ├── MultipleAppGlideModuleTest.java │ │ │ ├── MultipleEmptyLibraryGlideModuleTest.java │ │ │ ├── OverlyLongFileNameTest.java │ │ │ └── test/ │ │ │ ├── CompilationProvider.java │ │ │ ├── ReferencedResource.java │ │ │ ├── RegenerateResourcesRule.java │ │ │ ├── SubDirectory.java │ │ │ ├── TestDescription.java │ │ │ └── Util.java │ │ └── resources/ │ │ ├── AppGlideModuleWithExcludesTest/ │ │ │ ├── AppModuleWithExcludes.java │ │ │ └── GeneratedAppGlideModuleImpl.java │ │ ├── AppGlideModuleWithLibraryInPackageTest/ │ │ │ ├── AppModuleWithLibraryInPackage.java │ │ │ ├── GeneratedAppGlideModuleImpl.java │ │ │ └── LibraryModuleInPackage.java │ │ ├── AppGlideModuleWithMultipleExcludesTest/ │ │ │ ├── AppModuleWithMultipleExcludes.java │ │ │ ├── EmptyLibraryModule1.java │ │ │ ├── EmptyLibraryModule2.java │ │ │ └── GeneratedAppGlideModuleImpl.java │ │ ├── EmptyAppAndLibraryGlideModulesTest/ │ │ │ └── GeneratedAppGlideModuleImpl.java │ │ ├── EmptyAppGlideModuleTest/ │ │ │ ├── EmptyAppModule.java │ │ │ ├── GeneratedAppGlideModuleImpl.java │ │ │ ├── GeneratedRequestManagerFactory.java │ │ │ ├── GlideApp.java │ │ │ ├── GlideOptions.java │ │ │ ├── GlideRequest.java │ │ │ └── GlideRequests.java │ │ ├── EmptyLibraryGlideModuleTest/ │ │ │ ├── EmptyLibraryModule.java │ │ │ └── GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule.java │ │ ├── GlideExtensionOptionsTest/ │ │ │ ├── MemoizeStaticMethod/ │ │ │ │ ├── Extension.java │ │ │ │ ├── GlideOptions.java │ │ │ │ └── GlideRequest.java │ │ │ ├── OverrideExtend/ │ │ │ │ ├── Extension.java │ │ │ │ ├── GlideOptions.java │ │ │ │ └── GlideRequest.java │ │ │ ├── OverrideExtendMultipleArguments/ │ │ │ │ ├── Extension.java │ │ │ │ ├── GlideOptions.java │ │ │ │ └── GlideRequest.java │ │ │ ├── OverrideReplace/ │ │ │ │ ├── Extension.java │ │ │ │ ├── GlideOptions.java │ │ │ │ └── GlideRequest.java │ │ │ ├── SkipStaticMethod/ │ │ │ │ ├── Extension.java │ │ │ │ ├── GlideOptions.java │ │ │ │ └── GlideRequest.java │ │ │ └── StaticMethodName/ │ │ │ ├── Extension.java │ │ │ ├── GlideOptions.java │ │ │ └── GlideRequest.java │ │ ├── GlideExtensionWithOptionTest/ │ │ │ ├── ExtensionWithOption.java │ │ │ ├── GlideOptions.java │ │ │ └── GlideRequest.java │ │ ├── GlideExtensionWithTypeTest/ │ │ │ ├── ExtensionWithType.java │ │ │ ├── GlideOptions.java │ │ │ └── GlideRequests.java │ │ ├── MultipleAppGlideModuleTest/ │ │ │ ├── EmptyAppModule1.java │ │ │ └── EmptyAppModule2.java │ │ └── MultipleEmptyLibraryGlideModuleTest/ │ │ ├── EmptyLibraryModule1.java │ │ ├── EmptyLibraryModule2.java │ │ └── GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule1_com_bumptech_glide_test_EmptyLibraryModule2.java │ ├── gradle.properties │ ├── ksp/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── integrationtest/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── annotation/ │ │ │ └── ksp/ │ │ │ └── integrationtest/ │ │ │ └── IntegrationLibraryGlideModuleTests.kt │ │ ├── src/ │ │ │ └── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── annotation/ │ │ │ └── ksp/ │ │ │ ├── AppGlideModules.kt │ │ │ ├── GlideSymbolProcessor.kt │ │ │ ├── GlideSymbolProcessorProvider.kt │ │ │ ├── LibraryGlideModules.kt │ │ │ └── ModuleParser.kt │ │ └── test/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── annotation/ │ │ │ └── ksp/ │ │ │ └── test/ │ │ │ └── SourceTestHelpers.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── annotation/ │ │ └── ksp/ │ │ └── test/ │ │ ├── LibraryGlideModuleTests.kt │ │ └── OnlyAppGlideModuleTests.kt │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ └── annotation/ │ ├── Excludes.java │ ├── GlideExtension.java │ ├── GlideModule.java │ ├── GlideOption.java │ ├── GlideType.java │ ├── compiler/ │ │ └── Index.java │ └── ksp/ │ └── Index.java ├── benchmark/ │ ├── benchmark-proguard-rules.pro │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ └── androidTest/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ ├── benchmark/ │ │ ├── BenchmarkData.java │ │ ├── BenchmarkFromCache.java │ │ ├── BenchmarkMediaStoreData.java │ │ ├── BenchmarkModels.java │ │ ├── GlideBenchmarkRule.java │ │ └── data/ │ │ └── DataOpener.java │ └── load/ │ └── resource/ │ └── bitmap/ │ └── BenchmarkDownsampler.java ├── build.gradle ├── checkstyle.xml ├── checkstyle_suppressions.xml ├── gcloud-bumptech.json.enc ├── gcloud-sjudd.json.enc ├── glide/ │ ├── build.gradle │ └── gradle.properties ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── instrumentation/ │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ ├── AsBytesTest.java │ │ ├── AsFileTest.java │ │ ├── CachingTest.java │ │ ├── CenterCropRegressionTest.java │ │ ├── CenterInsideRegressionTest.java │ │ ├── CircleCropRegressionTest.java │ │ ├── DarkModeTest.java │ │ ├── DataUriTest.java │ │ ├── DownsampleVideoTest.java │ │ ├── DrawableTransformationTest.java │ │ ├── ErrorHandlingTest.java │ │ ├── ExternallyClearedDiskCacheTest.java │ │ ├── FitCenterRegressionTest.java │ │ ├── LargeImageTest.java │ │ ├── LoadAnimatedImageResourceTest.java │ │ ├── LoadAssetUriTest.java │ │ ├── LoadBitmapTest.java │ │ ├── LoadBytesTest.java │ │ ├── LoadDrawableTest.java │ │ ├── LoadResourcesWithDownsamplerTest.java │ │ ├── LoadVideoResourceTest.java │ │ ├── MultiRequestTest.java │ │ ├── NonBitmapDrawableResourcesTest.java │ │ ├── PausedRequestsTest.java │ │ ├── RequestManagerLifecycleTest.java │ │ ├── RequestManagerTest.java │ │ ├── RequestTest.java │ │ ├── RoundedCornersRegressionTest.java │ │ ├── WideGamutTest.java │ │ ├── load/ │ │ │ ├── engine/ │ │ │ │ └── executor/ │ │ │ │ └── IdlingGlideRule.java │ │ │ └── resource/ │ │ │ ├── bitmap/ │ │ │ │ └── DownsamplerEmulatorTest.java │ │ │ └── gif/ │ │ │ └── GifDrawableTest.java │ │ └── test/ │ │ ├── BitmapRegressionTester.java │ │ ├── CanonicalBitmap.java │ │ ├── ModelGeneratorRule.java │ │ ├── RegressionTest.java │ │ ├── ResourceIds.java │ │ ├── SplitByCpu.java │ │ └── SplitBySdk.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── test/ │ │ ├── DefaultFragmentActivity.java │ │ ├── ForceDarkOrLightModeActivity.java │ │ ├── GlideWithAsDifferentSupertypesActivity.java │ │ ├── GlideWithBeforeSuperOnCreateActivity.java │ │ └── InstrumentationAppGlideModule.java │ └── res/ │ ├── drawable/ │ │ ├── bitmap_alias.xml │ │ ├── shape_drawable.xml │ │ ├── state_list_drawable.xml │ │ ├── vector_drawable.xml │ │ ├── vector_drawable_dark.xml │ │ └── vector_drawable_light.xml │ ├── layout/ │ │ └── default_fragment_activity.xml │ ├── raw/ │ │ └── dl_world_anim_avif.avif │ ├── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── values-night/ │ └── colors.xml ├── integration/ │ ├── avif/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── avif/ │ │ ├── AvifByteBufferBitmapDecoder.java │ │ ├── AvifGlideModule.java │ │ └── AvifStreamBitmapDecoder.java │ ├── build.gradle.kts │ ├── compose/ │ │ ├── api/ │ │ │ └── compose.api │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ ├── integration/ │ │ │ │ └── compose/ │ │ │ │ ├── GlideImageCustomDrawableTransformationTest.kt │ │ │ │ ├── GlideImageDefaultTransformationTest.kt │ │ │ │ ├── GlideImageErrorTest.kt │ │ │ │ ├── GlideImagePlaceholderTest.kt │ │ │ │ ├── GlideImageTest.kt │ │ │ │ ├── RememberGlidePreloadingDataTest.kt │ │ │ │ └── test/ │ │ │ │ ├── GlideComposeRule.kt │ │ │ │ ├── expectations.kt │ │ │ │ └── nodes.kt │ │ │ └── load/ │ │ │ └── engine/ │ │ │ └── executor/ │ │ │ └── GlideIdlingResourceInit.kt │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── integration/ │ │ │ └── compose/ │ │ │ ├── ExperimentalGlideComposeApi.kt │ │ │ ├── GlideImage.kt │ │ │ ├── GlidePainter.kt │ │ │ ├── Preload.kt │ │ │ └── Sizes.kt │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── compose/ │ │ └── GlideImageTest.kt │ ├── concurrent/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── integration/ │ │ │ └── concurrent/ │ │ │ └── GlideFutures.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── concurrent/ │ │ └── GlideFuturesTest.java │ ├── cronet/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── integration/ │ │ │ └── cronet/ │ │ │ ├── BufferQueue.java │ │ │ ├── ByteBufferParser.java │ │ │ ├── ChromiumRequestSerializer.java │ │ │ ├── ChromiumUrlFetcher.java │ │ │ ├── ChromiumUrlLoader.java │ │ │ ├── CronetEngineSingleton.java │ │ │ ├── CronetGlideModule.java │ │ │ ├── CronetLibraryGlideModule.java │ │ │ ├── CronetRequestFactory.java │ │ │ ├── CronetRequestFactoryImpl.java │ │ │ └── DataLogger.java │ │ └── test/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── cronet/ │ │ └── ChromiumUrlFetcherTest.java │ ├── gifencoder/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── integration/ │ │ │ └── gifencoder/ │ │ │ └── ReEncodingGifResourceEncoder.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── gifencoder/ │ │ └── ReEncodingGifResourceEncoderTest.java │ ├── gradle.properties │ ├── ktx/ │ │ ├── api/ │ │ │ └── ktx.api │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ ├── GlideIntegration.kt │ │ │ └── integration/ │ │ │ └── ktx/ │ │ │ ├── Flows.kt │ │ │ └── InternalGlideApi.kt │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ ├── integration/ │ │ │ └── ktx/ │ │ │ └── FlowsTest.kt │ │ └── load/ │ │ └── engine/ │ │ └── executor/ │ │ └── GlideIdlingResourceInit.kt │ ├── okhttp/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── okhttp/ │ │ ├── OkHttpGlideModule.java │ │ ├── OkHttpLibraryGlideModule.java │ │ ├── OkHttpStreamFetcher.java │ │ └── OkHttpUrlLoader.java │ ├── okhttp3/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── okhttp3/ │ │ ├── OkHttpGlideModule.java │ │ ├── OkHttpLibraryGlideModule.java │ │ ├── OkHttpStreamFetcher.java │ │ └── OkHttpUrlLoader.java │ ├── okhttp4/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── okhttp3/ │ │ ├── OkHttpLibraryGlideModule.java │ │ ├── OkHttpStreamFetcher.java │ │ └── OkHttpUrlLoader.java │ ├── recyclerview/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── recyclerview/ │ │ ├── RecyclerToListViewScrollListener.java │ │ └── RecyclerViewPreloader.java │ ├── sqljournaldiskcache/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── integration/ │ │ │ └── sqljournaldiskcache/ │ │ │ ├── Clock.java │ │ │ ├── DefaultClock.java │ │ │ ├── DiskCacheDbHelper.java │ │ │ ├── EntryCache.java │ │ │ ├── EvictionManager.java │ │ │ ├── FileSystem.java │ │ │ ├── GlideJournaledLruDiskCacheWrapper.java │ │ │ ├── Journal.java │ │ │ ├── JournalTable.java │ │ │ ├── JournaledLruDiskCache.java │ │ │ ├── MessageIds.java │ │ │ ├── RecoveryManager.java │ │ │ ├── SizeJournal.java │ │ │ ├── SizeTable.java │ │ │ └── SqliteStatementPool.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── sqljournaldiskcache/ │ │ ├── DiskCacheDbHelperUpgradeTest.java │ │ ├── DiskCacheUtils.java │ │ ├── JournaledLruDiskCacheTest.java │ │ └── TestClock.java │ └── volley/ │ ├── build.gradle.kts │ ├── gradle.properties │ ├── lint.xml │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── integration/ │ │ └── volley/ │ │ ├── VolleyGlideModule.java │ │ ├── VolleyLibraryGlideModule.java │ │ ├── VolleyRequestFactory.java │ │ ├── VolleyStreamFetcher.java │ │ └── VolleyUrlLoader.java │ └── test/ │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ └── integration/ │ └── volley/ │ └── VolleyStreamFetcherServerTest.java ├── library/ │ ├── build.gradle │ ├── gradle.properties │ ├── lint.xml │ ├── pmd/ │ │ └── build.gradle │ ├── pmd-ruleset.xml │ ├── proguard-rules.txt │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── bumptech/ │ │ │ │ └── glide/ │ │ │ │ ├── GeneratedAppGlideModule.java │ │ │ │ ├── GenericTransitionOptions.java │ │ │ │ ├── Glide.java │ │ │ │ ├── GlideBuilder.java │ │ │ │ ├── GlideContext.java │ │ │ │ ├── GlideExperiments.java │ │ │ │ ├── ListPreloader.java │ │ │ │ ├── MemoryCategory.java │ │ │ │ ├── ModelTypes.java │ │ │ │ ├── Priority.java │ │ │ │ ├── Registry.java │ │ │ │ ├── RegistryFactory.java │ │ │ │ ├── RequestBuilder.java │ │ │ │ ├── RequestManager.java │ │ │ │ ├── TransitionOptions.java │ │ │ │ ├── load/ │ │ │ │ │ ├── DataSource.java │ │ │ │ │ ├── DecodeFormat.java │ │ │ │ │ ├── EncodeStrategy.java │ │ │ │ │ ├── Encoder.java │ │ │ │ │ ├── HttpException.java │ │ │ │ │ ├── ImageHeaderParser.java │ │ │ │ │ ├── ImageHeaderParserUtils.java │ │ │ │ │ ├── Key.java │ │ │ │ │ ├── MultiTransformation.java │ │ │ │ │ ├── Option.java │ │ │ │ │ ├── Options.java │ │ │ │ │ ├── PreferredColorSpace.java │ │ │ │ │ ├── ResourceDecoder.java │ │ │ │ │ ├── ResourceEncoder.java │ │ │ │ │ ├── Transformation.java │ │ │ │ │ ├── data/ │ │ │ │ │ │ ├── AssetFileDescriptorLocalUriFetcher.java │ │ │ │ │ │ ├── AssetPathFetcher.java │ │ │ │ │ │ ├── BufferedOutputStream.java │ │ │ │ │ │ ├── DataFetcher.java │ │ │ │ │ │ ├── DataRewinder.java │ │ │ │ │ │ ├── DataRewinderRegistry.java │ │ │ │ │ │ ├── ExifOrientationStream.java │ │ │ │ │ │ ├── FileDescriptorAssetPathFetcher.java │ │ │ │ │ │ ├── FileDescriptorLocalUriFetcher.java │ │ │ │ │ │ ├── HttpUrlFetcher.java │ │ │ │ │ │ ├── InputStreamRewinder.java │ │ │ │ │ │ ├── LocalUriFetcher.java │ │ │ │ │ │ ├── ParcelFileDescriptorRewinder.java │ │ │ │ │ │ ├── StreamAssetPathFetcher.java │ │ │ │ │ │ ├── StreamLocalUriFetcher.java │ │ │ │ │ │ └── mediastore/ │ │ │ │ │ │ ├── FileService.java │ │ │ │ │ │ ├── MediaStoreUtil.java │ │ │ │ │ │ ├── ThumbFetcher.java │ │ │ │ │ │ ├── ThumbnailQuery.java │ │ │ │ │ │ └── ThumbnailStreamOpener.java │ │ │ │ │ ├── engine/ │ │ │ │ │ │ ├── ActiveResources.java │ │ │ │ │ │ ├── CallbackException.java │ │ │ │ │ │ ├── DataCacheGenerator.java │ │ │ │ │ │ ├── DataCacheKey.java │ │ │ │ │ │ ├── DataCacheWriter.java │ │ │ │ │ │ ├── DataFetcherGenerator.java │ │ │ │ │ │ ├── DecodeHelper.java │ │ │ │ │ │ ├── DecodeJob.java │ │ │ │ │ │ ├── DecodePath.java │ │ │ │ │ │ ├── DiskCacheStrategy.java │ │ │ │ │ │ ├── Engine.java │ │ │ │ │ │ ├── EngineJob.java │ │ │ │ │ │ ├── EngineJobListener.java │ │ │ │ │ │ ├── EngineKey.java │ │ │ │ │ │ ├── EngineKeyFactory.java │ │ │ │ │ │ ├── EngineResource.java │ │ │ │ │ │ ├── GlideException.java │ │ │ │ │ │ ├── Initializable.java │ │ │ │ │ │ ├── Jobs.java │ │ │ │ │ │ ├── LoadPath.java │ │ │ │ │ │ ├── LockedResource.java │ │ │ │ │ │ ├── Resource.java │ │ │ │ │ │ ├── ResourceCacheGenerator.java │ │ │ │ │ │ ├── ResourceCacheKey.java │ │ │ │ │ │ ├── ResourceRecycler.java │ │ │ │ │ │ ├── SourceGenerator.java │ │ │ │ │ │ ├── bitmap_recycle/ │ │ │ │ │ │ │ ├── ArrayAdapterInterface.java │ │ │ │ │ │ │ ├── ArrayPool.java │ │ │ │ │ │ │ ├── AttributeStrategy.java │ │ │ │ │ │ │ ├── BaseKeyPool.java │ │ │ │ │ │ │ ├── BitmapPool.java │ │ │ │ │ │ │ ├── BitmapPoolAdapter.java │ │ │ │ │ │ │ ├── ByteArrayAdapter.java │ │ │ │ │ │ │ ├── GroupedLinkedMap.java │ │ │ │ │ │ │ ├── IntegerArrayAdapter.java │ │ │ │ │ │ │ ├── LruArrayPool.java │ │ │ │ │ │ │ ├── LruBitmapPool.java │ │ │ │ │ │ │ ├── LruPoolStrategy.java │ │ │ │ │ │ │ ├── Poolable.java │ │ │ │ │ │ │ ├── PrettyPrintTreeMap.java │ │ │ │ │ │ │ ├── SizeConfigStrategy.java │ │ │ │ │ │ │ └── SizeStrategy.java │ │ │ │ │ │ ├── cache/ │ │ │ │ │ │ │ ├── DiskCache.java │ │ │ │ │ │ │ ├── DiskCacheAdapter.java │ │ │ │ │ │ │ ├── DiskCacheWriteLocker.java │ │ │ │ │ │ │ ├── DiskLruCacheFactory.java │ │ │ │ │ │ │ ├── DiskLruCacheWrapper.java │ │ │ │ │ │ │ ├── ExternalCacheDiskCacheFactory.java │ │ │ │ │ │ │ ├── ExternalPreferredCacheDiskCacheFactory.java │ │ │ │ │ │ │ ├── InternalCacheDiskCacheFactory.java │ │ │ │ │ │ │ ├── LruResourceCache.java │ │ │ │ │ │ │ ├── MemoryCache.java │ │ │ │ │ │ │ ├── MemoryCacheAdapter.java │ │ │ │ │ │ │ ├── MemorySizeCalculator.java │ │ │ │ │ │ │ └── SafeKeyGenerator.java │ │ │ │ │ │ ├── executor/ │ │ │ │ │ │ │ ├── GlideExecutor.java │ │ │ │ │ │ │ └── RuntimeCompat.java │ │ │ │ │ │ └── prefill/ │ │ │ │ │ │ ├── BitmapPreFillRunner.java │ │ │ │ │ │ ├── BitmapPreFiller.java │ │ │ │ │ │ ├── PreFillQueue.java │ │ │ │ │ │ └── PreFillType.java │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── AssetUriLoader.java │ │ │ │ │ │ ├── ByteArrayLoader.java │ │ │ │ │ │ ├── ByteBufferEncoder.java │ │ │ │ │ │ ├── ByteBufferFileLoader.java │ │ │ │ │ │ ├── DataUrlLoader.java │ │ │ │ │ │ ├── DirectResourceLoader.java │ │ │ │ │ │ ├── FileLoader.java │ │ │ │ │ │ ├── GlideUrl.java │ │ │ │ │ │ ├── Headers.java │ │ │ │ │ │ ├── LazyHeaderFactory.java │ │ │ │ │ │ ├── LazyHeaders.java │ │ │ │ │ │ ├── MediaStoreFileLoader.java │ │ │ │ │ │ ├── Model.java │ │ │ │ │ │ ├── ModelCache.java │ │ │ │ │ │ ├── ModelLoader.java │ │ │ │ │ │ ├── ModelLoaderFactory.java │ │ │ │ │ │ ├── ModelLoaderRegistry.java │ │ │ │ │ │ ├── MultiModelLoader.java │ │ │ │ │ │ ├── MultiModelLoaderFactory.java │ │ │ │ │ │ ├── ResourceLoader.java │ │ │ │ │ │ ├── ResourceUriLoader.java │ │ │ │ │ │ ├── StreamEncoder.java │ │ │ │ │ │ ├── StringLoader.java │ │ │ │ │ │ ├── UnitModelLoader.java │ │ │ │ │ │ ├── UriLoader.java │ │ │ │ │ │ ├── UrlUriLoader.java │ │ │ │ │ │ └── stream/ │ │ │ │ │ │ ├── BaseGlideUrlLoader.java │ │ │ │ │ │ ├── HttpGlideUrlLoader.java │ │ │ │ │ │ ├── HttpUriLoader.java │ │ │ │ │ │ ├── MediaStoreImageThumbLoader.java │ │ │ │ │ │ ├── MediaStoreVideoThumbLoader.java │ │ │ │ │ │ ├── QMediaStoreUriLoader.java │ │ │ │ │ │ └── UrlLoader.java │ │ │ │ │ └── resource/ │ │ │ │ │ ├── DefaultOnHeaderDecodedListener.java │ │ │ │ │ ├── SimpleResource.java │ │ │ │ │ ├── UnitTransformation.java │ │ │ │ │ ├── bitmap/ │ │ │ │ │ │ ├── BitmapDrawableDecoder.java │ │ │ │ │ │ ├── BitmapDrawableEncoder.java │ │ │ │ │ │ ├── BitmapDrawableResource.java │ │ │ │ │ │ ├── BitmapDrawableTransformation.java │ │ │ │ │ │ ├── BitmapEncoder.java │ │ │ │ │ │ ├── BitmapImageDecoderResourceDecoder.java │ │ │ │ │ │ ├── BitmapResource.java │ │ │ │ │ │ ├── BitmapTransformation.java │ │ │ │ │ │ ├── BitmapTransitionOptions.java │ │ │ │ │ │ ├── ByteBufferBitmapDecoder.java │ │ │ │ │ │ ├── ByteBufferBitmapImageDecoderResourceDecoder.java │ │ │ │ │ │ ├── CenterCrop.java │ │ │ │ │ │ ├── CenterInside.java │ │ │ │ │ │ ├── CircleCrop.java │ │ │ │ │ │ ├── DefaultImageHeaderParser.java │ │ │ │ │ │ ├── DownsampleStrategy.java │ │ │ │ │ │ ├── Downsampler.java │ │ │ │ │ │ ├── DrawableToBitmapConverter.java │ │ │ │ │ │ ├── DrawableTransformation.java │ │ │ │ │ │ ├── ExifInterfaceImageHeaderParser.java │ │ │ │ │ │ ├── FitCenter.java │ │ │ │ │ │ ├── GlideBitmapFactory.java │ │ │ │ │ │ ├── GranularRoundedCorners.java │ │ │ │ │ │ ├── HardwareConfigState.java │ │ │ │ │ │ ├── ImageReader.java │ │ │ │ │ │ ├── InputStreamBitmapImageDecoderResourceDecoder.java │ │ │ │ │ │ ├── LazyBitmapDrawableResource.java │ │ │ │ │ │ ├── ParcelFileDescriptorBitmapDecoder.java │ │ │ │ │ │ ├── RecyclableBufferedInputStream.java │ │ │ │ │ │ ├── ResourceBitmapDecoder.java │ │ │ │ │ │ ├── Rotate.java │ │ │ │ │ │ ├── RoundedCorners.java │ │ │ │ │ │ ├── StreamBitmapDecoder.java │ │ │ │ │ │ ├── TransformationUtils.java │ │ │ │ │ │ ├── UnitBitmapDecoder.java │ │ │ │ │ │ ├── VideoBitmapDecoder.java │ │ │ │ │ │ └── VideoDecoder.java │ │ │ │ │ ├── bytes/ │ │ │ │ │ │ ├── ByteBufferRewinder.java │ │ │ │ │ │ └── BytesResource.java │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ ├── AnimatedImageDecoder.java │ │ │ │ │ │ ├── AnimatedWebpDecoder.java │ │ │ │ │ │ ├── DrawableDecoderCompat.java │ │ │ │ │ │ ├── DrawableResource.java │ │ │ │ │ │ ├── DrawableTransitionOptions.java │ │ │ │ │ │ ├── NonOwnedDrawableResource.java │ │ │ │ │ │ ├── ResourceDrawableDecoder.java │ │ │ │ │ │ └── UnitDrawableDecoder.java │ │ │ │ │ ├── file/ │ │ │ │ │ │ ├── FileDecoder.java │ │ │ │ │ │ └── FileResource.java │ │ │ │ │ ├── gif/ │ │ │ │ │ │ ├── ByteBufferGifDecoder.java │ │ │ │ │ │ ├── GifBitmapProvider.java │ │ │ │ │ │ ├── GifDrawable.java │ │ │ │ │ │ ├── GifDrawableEncoder.java │ │ │ │ │ │ ├── GifDrawableResource.java │ │ │ │ │ │ ├── GifDrawableTransformation.java │ │ │ │ │ │ ├── GifFrameLoader.java │ │ │ │ │ │ ├── GifFrameResourceDecoder.java │ │ │ │ │ │ ├── GifOptions.java │ │ │ │ │ │ └── StreamGifDecoder.java │ │ │ │ │ └── transcode/ │ │ │ │ │ ├── BitmapBytesTranscoder.java │ │ │ │ │ ├── BitmapDrawableTranscoder.java │ │ │ │ │ ├── DrawableBytesTranscoder.java │ │ │ │ │ ├── GifDrawableBytesTranscoder.java │ │ │ │ │ ├── ResourceTranscoder.java │ │ │ │ │ ├── TranscoderRegistry.java │ │ │ │ │ └── UnitTranscoder.java │ │ │ │ ├── manager/ │ │ │ │ │ ├── ApplicationLifecycle.java │ │ │ │ │ ├── ConnectivityMonitor.java │ │ │ │ │ ├── ConnectivityMonitorFactory.java │ │ │ │ │ ├── DefaultConnectivityMonitor.java │ │ │ │ │ ├── DefaultConnectivityMonitorFactory.java │ │ │ │ │ ├── DoNothingFirstFrameWaiter.java │ │ │ │ │ ├── EmptyRequestManagerTreeNode.java │ │ │ │ │ ├── FirstFrameWaiter.java │ │ │ │ │ ├── FrameWaiter.java │ │ │ │ │ ├── Lifecycle.java │ │ │ │ │ ├── LifecycleLifecycle.java │ │ │ │ │ ├── LifecycleListener.java │ │ │ │ │ ├── LifecycleRequestManagerRetriever.java │ │ │ │ │ ├── NullConnectivityMonitor.java │ │ │ │ │ ├── RequestManagerFragment.java │ │ │ │ │ ├── RequestManagerRetriever.java │ │ │ │ │ ├── RequestManagerTreeNode.java │ │ │ │ │ ├── RequestTracker.java │ │ │ │ │ ├── SingletonConnectivityReceiver.java │ │ │ │ │ ├── SupportRequestManagerFragment.java │ │ │ │ │ └── TargetTracker.java │ │ │ │ ├── module/ │ │ │ │ │ ├── AppGlideModule.java │ │ │ │ │ ├── AppliesOptions.java │ │ │ │ │ ├── GlideModule.java │ │ │ │ │ ├── LibraryGlideModule.java │ │ │ │ │ ├── ManifestParser.java │ │ │ │ │ └── RegistersComponents.java │ │ │ │ ├── provider/ │ │ │ │ │ ├── EncoderRegistry.java │ │ │ │ │ ├── ImageHeaderParserRegistry.java │ │ │ │ │ ├── LoadPathCache.java │ │ │ │ │ ├── ModelToResourceClassCache.java │ │ │ │ │ ├── ResourceDecoderRegistry.java │ │ │ │ │ └── ResourceEncoderRegistry.java │ │ │ │ ├── request/ │ │ │ │ │ ├── BaseRequestOptions.java │ │ │ │ │ ├── ErrorRequestCoordinator.java │ │ │ │ │ ├── ExperimentalRequestListener.java │ │ │ │ │ ├── FutureTarget.java │ │ │ │ │ ├── Request.java │ │ │ │ │ ├── RequestCoordinator.java │ │ │ │ │ ├── RequestFutureTarget.java │ │ │ │ │ ├── RequestListener.java │ │ │ │ │ ├── RequestOptions.java │ │ │ │ │ ├── ResourceCallback.java │ │ │ │ │ ├── SingleRequest.java │ │ │ │ │ ├── ThumbnailRequestCoordinator.java │ │ │ │ │ ├── target/ │ │ │ │ │ │ ├── AppWidgetTarget.java │ │ │ │ │ │ ├── BaseTarget.java │ │ │ │ │ │ ├── BitmapImageViewTarget.java │ │ │ │ │ │ ├── BitmapThumbnailImageViewTarget.java │ │ │ │ │ │ ├── CustomTarget.java │ │ │ │ │ │ ├── CustomViewTarget.java │ │ │ │ │ │ ├── DrawableImageViewTarget.java │ │ │ │ │ │ ├── DrawableThumbnailImageViewTarget.java │ │ │ │ │ │ ├── FixedSizeDrawable.java │ │ │ │ │ │ ├── ImageViewTarget.java │ │ │ │ │ │ ├── ImageViewTargetFactory.java │ │ │ │ │ │ ├── NotificationTarget.java │ │ │ │ │ │ ├── PreloadTarget.java │ │ │ │ │ │ ├── SimpleTarget.java │ │ │ │ │ │ ├── SizeReadyCallback.java │ │ │ │ │ │ ├── Target.java │ │ │ │ │ │ ├── ThumbnailImageViewTarget.java │ │ │ │ │ │ └── ViewTarget.java │ │ │ │ │ └── transition/ │ │ │ │ │ ├── BitmapContainerTransitionFactory.java │ │ │ │ │ ├── BitmapTransitionFactory.java │ │ │ │ │ ├── DrawableCrossFadeFactory.java │ │ │ │ │ ├── DrawableCrossFadeTransition.java │ │ │ │ │ ├── NoTransition.java │ │ │ │ │ ├── Transition.java │ │ │ │ │ ├── TransitionFactory.java │ │ │ │ │ ├── ViewAnimationFactory.java │ │ │ │ │ ├── ViewPropertyAnimationFactory.java │ │ │ │ │ ├── ViewPropertyTransition.java │ │ │ │ │ └── ViewTransition.java │ │ │ │ ├── signature/ │ │ │ │ │ ├── AndroidResourceSignature.java │ │ │ │ │ ├── ApplicationVersionSignature.java │ │ │ │ │ ├── EmptySignature.java │ │ │ │ │ ├── MediaStoreSignature.java │ │ │ │ │ └── ObjectKey.java │ │ │ │ └── util/ │ │ │ │ ├── ByteBufferUtil.java │ │ │ │ ├── CachedHashCodeArrayMap.java │ │ │ │ ├── ContentLengthInputStream.java │ │ │ │ ├── ExceptionCatchingInputStream.java │ │ │ │ ├── ExceptionPassthroughInputStream.java │ │ │ │ ├── Executors.java │ │ │ │ ├── FixedPreloadSizeProvider.java │ │ │ │ ├── GlideSuppliers.java │ │ │ │ ├── LogTime.java │ │ │ │ ├── LruCache.java │ │ │ │ ├── MarkEnforcingInputStream.java │ │ │ │ ├── MultiClassKey.java │ │ │ │ ├── Preconditions.java │ │ │ │ ├── Synthetic.java │ │ │ │ ├── Util.java │ │ │ │ ├── ViewPreloadSizeProvider.java │ │ │ │ └── pool/ │ │ │ │ ├── FactoryPools.java │ │ │ │ ├── GlideTrace.java │ │ │ │ └── StateVerifier.java │ │ │ └── res/ │ │ │ └── values/ │ │ │ └── ids.xml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── request/ │ │ └── target/ │ │ └── CustomViewTargetTest.java │ └── test/ │ ├── build.gradle.kts │ └── src/ │ └── test/ │ ├── java/ │ │ ├── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ ├── GlideContextTest.java │ │ │ ├── GlideTest.java │ │ │ ├── InitializeGlideTest.java │ │ │ ├── ListPreloaderTest.java │ │ │ ├── RegistryFactoryTest.java │ │ │ ├── RegistryTest.java │ │ │ ├── RequestBuilderTest.java │ │ │ ├── RequestManagerTest.java │ │ │ ├── load/ │ │ │ │ ├── ImageHeaderParserUtilsTest.java │ │ │ │ ├── MultiTransformationTest.java │ │ │ │ ├── OptionsTest.java │ │ │ │ ├── data/ │ │ │ │ │ ├── BufferedOutputStreamFuzzTest.java │ │ │ │ │ ├── BufferedOutputStreamTest.java │ │ │ │ │ ├── ExifOrientationStreamTest.java │ │ │ │ │ ├── FileDescriptorAssetPathFetcherTest.java │ │ │ │ │ ├── HttpUrlFetcherServerTest.java │ │ │ │ │ ├── HttpUrlFetcherTest.java │ │ │ │ │ ├── LocalUriFetcherTest.java │ │ │ │ │ ├── StreamAssetPathFetcherTest.java │ │ │ │ │ ├── mediastore/ │ │ │ │ │ │ ├── MediaStoreUtilTest.java │ │ │ │ │ │ ├── ThumbFetcherTest.java │ │ │ │ │ │ └── ThumbnailStreamOpenerTest.java │ │ │ │ │ └── resource/ │ │ │ │ │ ├── FileDescriptorLocalUriFetcherTest.java │ │ │ │ │ └── StreamLocalUriFetcherTest.java │ │ │ │ ├── engine/ │ │ │ │ │ ├── ActiveResourcesTest.java │ │ │ │ │ ├── DataCacheKeyTest.java │ │ │ │ │ ├── EngineJobTest.java │ │ │ │ │ ├── EngineKeyTest.java │ │ │ │ │ ├── EngineResourceTest.java │ │ │ │ │ ├── EngineTest.java │ │ │ │ │ ├── ResourceCacheKeyTest.java │ │ │ │ │ ├── ResourceRecyclerTest.java │ │ │ │ │ ├── bitmap_recycle/ │ │ │ │ │ │ ├── AttributeStrategyKeyTest.java │ │ │ │ │ │ ├── AttributeStrategyTest.java │ │ │ │ │ │ ├── GroupedLinkedMapTest.java │ │ │ │ │ │ ├── LruArrayPoolTest.java │ │ │ │ │ │ ├── LruBitmapPoolTest.java │ │ │ │ │ │ ├── SizeConfigStrategyTest.java │ │ │ │ │ │ └── SizeStrategyKeyTest.java │ │ │ │ │ ├── cache/ │ │ │ │ │ │ ├── DiskLruCacheWrapperTest.java │ │ │ │ │ │ ├── LruCacheTest.java │ │ │ │ │ │ ├── LruResourceCacheTest.java │ │ │ │ │ │ ├── MemorySizeCalculatorTest.java │ │ │ │ │ │ └── SafeKeyGeneratorTest.java │ │ │ │ │ ├── executor/ │ │ │ │ │ │ └── GlideExecutorTest.java │ │ │ │ │ └── prefill/ │ │ │ │ │ ├── BitmapPreFillRunnerTest.java │ │ │ │ │ ├── BitmapPreFillerTest.java │ │ │ │ │ └── PreFillTypeTest.java │ │ │ │ ├── model/ │ │ │ │ │ ├── AssetUriLoaderTest.java │ │ │ │ │ ├── ByteArrayLoaderTest.java │ │ │ │ │ ├── DataUrlLoaderTest.java │ │ │ │ │ ├── GlideUrlTest.java │ │ │ │ │ ├── LazyHeadersTest.java │ │ │ │ │ ├── ModelCacheTest.java │ │ │ │ │ ├── ModelLoaderRegistryTest.java │ │ │ │ │ ├── MultiModelLoaderFactoryTest.java │ │ │ │ │ ├── ResourceLoaderTest.java │ │ │ │ │ ├── StreamEncoderTest.java │ │ │ │ │ ├── StringLoaderTest.java │ │ │ │ │ ├── UriLoaderTest.java │ │ │ │ │ ├── UrlUriLoaderTest.java │ │ │ │ │ └── stream/ │ │ │ │ │ ├── BaseGlideUrlLoaderTest.java │ │ │ │ │ └── HttpGlideUrlLoaderTest.java │ │ │ │ └── resource/ │ │ │ │ ├── SimpleResourceTest.java │ │ │ │ ├── UnitTransformationTest.java │ │ │ │ ├── bitmap/ │ │ │ │ │ ├── BitmapDrawableResourceTest.java │ │ │ │ │ ├── BitmapDrawableTransformationTest.java │ │ │ │ │ ├── BitmapEncoderTest.java │ │ │ │ │ ├── BitmapResourceTest.java │ │ │ │ │ ├── BitmapTransformationTest.java │ │ │ │ │ ├── CenterCropTest.java │ │ │ │ │ ├── CenterInsideTest.java │ │ │ │ │ ├── CircleCropTest.java │ │ │ │ │ ├── DefaultImageHeaderParserTest.java │ │ │ │ │ ├── DownsampleStrategyTest.java │ │ │ │ │ ├── DrawableTransformationTest.java │ │ │ │ │ ├── FitCenterTest.java │ │ │ │ │ ├── HardwareConfigStateTest.java │ │ │ │ │ ├── LazyBitmapDrawableResourceTest.java │ │ │ │ │ ├── RecyclableBufferedInputStreamTest.java │ │ │ │ │ ├── TransformationUtilsTest.java │ │ │ │ │ └── VideoDecoderTest.java │ │ │ │ ├── bytes/ │ │ │ │ │ └── BytesResourceTest.java │ │ │ │ ├── drawable/ │ │ │ │ │ └── DrawableResourceTest.java │ │ │ │ ├── file/ │ │ │ │ │ ├── FileDecoderTest.java │ │ │ │ │ └── FileResourceTest.java │ │ │ │ ├── gif/ │ │ │ │ │ ├── ByteBufferGifDecoderTest.java │ │ │ │ │ ├── GifDrawableResourceTest.java │ │ │ │ │ ├── GifDrawableTest.java │ │ │ │ │ ├── GifDrawableTransformationTest.java │ │ │ │ │ ├── GifFrameLoaderTest.java │ │ │ │ │ ├── GifFrameResourceDecoderTest.java │ │ │ │ │ └── StreamGifDecoderTest.java │ │ │ │ └── transcode/ │ │ │ │ ├── BitmapBytesTranscoderTest.java │ │ │ │ ├── BitmapDrawableTranscoderTest.java │ │ │ │ ├── GifDrawableBytesTranscoderTest.java │ │ │ │ ├── TranscoderRegistryTest.java │ │ │ │ └── UnitTranscoderTest.java │ │ │ ├── manager/ │ │ │ │ ├── DefaultConnectivityMonitorFactoryTest.java │ │ │ │ ├── DefaultConnectivityMonitorTest.java │ │ │ │ ├── Issue117Activity.java │ │ │ │ ├── RequestManagerRetrieverTest.java │ │ │ │ └── RequestTrackerTest.java │ │ │ ├── module/ │ │ │ │ └── ManifestParserTest.java │ │ │ ├── request/ │ │ │ │ ├── ErrorRequestCoordinatorTest.java │ │ │ │ ├── RequestFutureTargetTest.java │ │ │ │ ├── RequestOptionsTest.java │ │ │ │ ├── SingleRequestTest.java │ │ │ │ ├── ThumbnailRequestCoordinatorTest.java │ │ │ │ ├── target/ │ │ │ │ │ ├── AppWidgetTargetTest.java │ │ │ │ │ ├── BitmapImageViewTargetTest.java │ │ │ │ │ ├── ImageViewTargetFactoryTest.java │ │ │ │ │ ├── ImageViewTargetTest.java │ │ │ │ │ ├── NotificationTargetTest.java │ │ │ │ │ ├── PreloadTargetTest.java │ │ │ │ │ ├── SimpleTargetTest.java │ │ │ │ │ └── ViewTargetTest.java │ │ │ │ └── transition/ │ │ │ │ ├── DrawableCrossFadeFactoryTest.java │ │ │ │ ├── DrawableCrossFadeViewAnimationTest.java │ │ │ │ ├── ViewAnimationTest.java │ │ │ │ ├── ViewPropertyAnimationTest.java │ │ │ │ ├── ViewPropertyViewTransitionAnimationFactoryTest.java │ │ │ │ └── ViewTransitionAnimationFactoryTest.java │ │ │ ├── resize/ │ │ │ │ └── load/ │ │ │ │ └── ExifTest.java │ │ │ ├── signature/ │ │ │ │ ├── AndroidResourceSignatureTest.java │ │ │ │ ├── ApplicationVersionSignatureTest.java │ │ │ │ ├── EmptySignatureTest.java │ │ │ │ ├── MediaStoreSignatureTest.java │ │ │ │ └── ObjectKeyTest.java │ │ │ ├── tests/ │ │ │ │ ├── BackgroundUtil.java │ │ │ │ ├── ContentResolverShadow.java │ │ │ │ ├── GlideShadowLog.java │ │ │ │ ├── KeyTester.java │ │ │ │ ├── TearDownGlide.java │ │ │ │ └── Util.java │ │ │ └── util/ │ │ │ ├── ByteBufferUtilTest.java │ │ │ ├── ContentLengthInputStreamTest.java │ │ │ ├── ExceptionPassthroughInputStreamTest.java │ │ │ ├── FixedPreloadSizeProviderTest.java │ │ │ ├── MarkEnforcingInputStreamTest.java │ │ │ ├── UtilTest.java │ │ │ └── ViewPreloadSizeProviderTest.java │ │ └── opengles/ │ │ └── GL.java │ └── resources/ │ ├── animated_avif.avif │ └── org.robolectric.Config.properties ├── mocks/ │ ├── build.gradle.kts │ ├── gradle.properties │ ├── lint.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ ├── load/ │ │ └── engine/ │ │ └── executor/ │ │ └── MockGlideExecutor.java │ └── mocks/ │ ├── AnswerSelf.java │ └── MockGlideBuilders.java ├── renovate.json ├── samples/ │ ├── contacturi/ │ │ ├── build.gradle.kts │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── samples/ │ │ │ └── contacturi/ │ │ │ ├── ContactUriModule.java │ │ │ └── MainActivity.java │ │ └── res/ │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ └── values/ │ │ ├── dimens.xml │ │ └── strings.xml │ ├── flickr/ │ │ ├── build.gradle.kts │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── samples/ │ │ │ └── flickr/ │ │ │ ├── FlickrGlideExtension.java │ │ │ ├── FlickrGlideModule.java │ │ │ ├── FlickrModelLoader.java │ │ │ ├── FlickrPhotoGrid.java │ │ │ ├── FlickrPhotoList.java │ │ │ ├── FlickrSearchActivity.java │ │ │ ├── FullscreenActivity.java │ │ │ ├── PhotoViewer.java │ │ │ ├── SquareImageView.java │ │ │ └── api/ │ │ │ ├── Api.java │ │ │ ├── FlickrQueryResponseListener.java │ │ │ ├── Photo.java │ │ │ ├── PhotoJsonStringParser.java │ │ │ ├── Query.java │ │ │ ├── RecentQuery.java │ │ │ └── SearchQuery.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── flickr_photo_grid.xml │ │ │ ├── flickr_photo_grid_item.xml │ │ │ ├── flickr_photo_list.xml │ │ │ ├── flickr_photo_list_item.xml │ │ │ ├── flickr_search_activity.xml │ │ │ └── fullscreen_activity.xml │ │ ├── menu/ │ │ │ └── search_activity.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ └── strings.xml │ │ └── xml/ │ │ └── network_security_config.xml │ ├── gallery/ │ │ ├── build.gradle.kts │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── samples/ │ │ │ └── gallery/ │ │ │ ├── GalleryModule.kt │ │ │ ├── GalleryViewModel.kt │ │ │ ├── HorizontalGalleryFragment.kt │ │ │ ├── MainActivity.kt │ │ │ └── MediaStoreDataSource.kt │ │ └── res/ │ │ ├── layout/ │ │ │ ├── main_activity.xml │ │ │ ├── recycler_item.xml │ │ │ └── recycler_view.xml │ │ └── values/ │ │ ├── ids.xml │ │ └── strings.xml │ ├── giphy/ │ │ ├── build.gradle.kts │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── samples/ │ │ │ └── giphy/ │ │ │ ├── Api.java │ │ │ ├── FullscreenActivity.java │ │ │ ├── GiphyGlideModule.java │ │ │ ├── GiphyModelLoader.java │ │ │ └── MainActivity.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── fullscreen_activity.xml │ │ │ └── gif_list_item.xml │ │ └── values/ │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── imgur/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── lint.xml │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── bumptech/ │ │ │ └── glide/ │ │ │ └── samples/ │ │ │ └── imgur/ │ │ │ ├── ApplicationModule.java │ │ │ ├── ImgurApplication.java │ │ │ ├── ImgurApplicationComponent.java │ │ │ ├── ImgurGlideModule.java │ │ │ ├── MainActivity.java │ │ │ ├── MainActivityModule.java │ │ │ └── api/ │ │ │ ├── ApiModule.java │ │ │ ├── Gallery.java │ │ │ ├── Image.java │ │ │ ├── ImgurObservables.java │ │ │ └── ImgurService.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ └── image_card.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── xml/ │ │ └── network_security_config.xml │ └── svg/ │ ├── build.gradle.kts │ ├── lint.xml │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── samples/ │ │ └── svg/ │ │ ├── MainActivity.java │ │ ├── SvgDecoder.java │ │ ├── SvgDrawableTranscoder.java │ │ ├── SvgModule.java │ │ └── SvgSoftwareLayerSetter.java │ └── res/ │ ├── drawable/ │ │ ├── dot_dot_dot.xml │ │ ├── image_error.xml │ │ └── image_loading.xml │ ├── layout/ │ │ └── activity_main.xml │ ├── values/ │ │ ├── dimens.xml │ │ └── strings.xml │ └── values-w820dp/ │ └── dimens.xml ├── scripts/ │ ├── run_instrumentation_tests.sh │ ├── update_javadocs.sh │ └── upload.gradle.kts ├── settings.gradle.kts ├── static/ │ └── logo-styles.css ├── testutil/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ ├── RobolectricConstants.java │ └── testutil/ │ ├── BitmapSubject.java │ ├── ConcurrencyHelper.java │ ├── MockModelLoader.java │ ├── TearDownGlide.java │ ├── TestResourceUtil.java │ ├── TestUtil.java │ ├── WaitModelLoader.java │ └── WaitModelLoaderRule.java └── third_party/ ├── disklrucache/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── LICENSE.txt │ ├── README.md │ ├── THIRD_PARTY.md │ ├── build.gradle.kts │ ├── checkstyle.xml │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── disklrucache/ │ │ ├── DiskLruCache.java │ │ ├── StrictLineReader.java │ │ └── Util.java │ └── test/ │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ └── disklrucache/ │ ├── DiskLruCacheTest.java │ └── StrictLineReaderTest.java ├── gif_decoder/ │ ├── LICENSE │ ├── THIRD_PARTY.md │ ├── build.gradle.kts │ ├── gradle.properties │ ├── lint.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── bumptech/ │ │ └── glide/ │ │ └── gifdecoder/ │ │ ├── GifDecoder.java │ │ ├── GifFrame.java │ │ ├── GifHeader.java │ │ ├── GifHeaderParser.java │ │ └── StandardGifDecoder.java │ └── test/ │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ └── gifdecoder/ │ ├── GifDecoderTest.java │ ├── GifHeaderParserTest.java │ └── test/ │ ├── GifBytesTestUtil.java │ └── GifBytesTestUtilTest.java ├── gif_encoder/ │ ├── LICENSE │ ├── THIRD_PARTY.md │ ├── lint.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── bumptech/ │ └── glide/ │ └── gifencoder/ │ ├── AnimatedGifEncoder.java │ ├── LZWEncoder.java │ └── NeuQuant.java └── gradle.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 7 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - bug - enhancement - feature - documentation - build stability # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had activity in the last seven days. It will be closed if no further activity occurs within the next seven days. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false unmarkComment: false ================================================ FILE: .github/workflows/build.yml ================================================ name: Android CI on: push: pull_request: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout project uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: "zulu" java-version: "17" - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Run Gradle Build run: | ./gradlew build \ -x :library:test:testDebugUnitTest \ :library:test:assembleDebugUnitTest \ -x :library:testDebugUnitTest \ :library:assembleDebugUnitTest \ -x :annotation:ksp:test:testDebugUnitTest \ :annotation:ksp:test:assembleDebugUnitTest \ -x :third_party:disklrucache:testDebugUnitTest \ :third_party:disklrucache:assembleDebugUnitTest \ -x :integration:cronet:testDebugUnitTest \ :integration:cronet:assembleDebugUnitTest \ -x :integration:gifencoder:testDebugUnitTest \ :integration:gifencoder:assembleDebugUnitTest \ -x :integration:ktx:testDebugUnitTest \ :integration:ktx:assembleDebugUnitTest \ -x :integration:concurrent:testDebugUnitTest \ :integration:concurrent:assembleDebugUnitTest \ -x :integration:volley:testDebugUnitTest \ :integration:volley:assembleDebugUnitTest \ -x :integration:sqljournaldiskcache:testDebugUnitTest \ :integration:sqljournaldiskcache:assembleDebugUnitTest \ -x :third_party:gif_decoder:testDebugUnitTest \ :third_party:gif_decoder:assembleDebugUnitTest \ :samples:flickr:build \ :samples:giphy:build \ :samples:contacturi:build \ :samples:gallery:build \ :samples:imgur:build \ :samples:svg:build \ :instrumentation:assembleAndroidTest \ :benchmark:assembleAndroidTest \ :glide:releaseJavadoc \ :annotation:ksp:test:test \ :integration:ktx:apiCheck \ :annotation:ksp:integrationtest:test \ --parallel ================================================ FILE: .github/workflows/publish-manual.yml ================================================ name: Publish to Maven (manual) on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout project uses: actions/checkout@v4 - name: Make Gradle wrapper executable run: chmod +x ./gradlew - uses: actions/setup-java@v4 with: distribution: "zulu" java-version: "17" - name: Build and publish everything to Maven Central # This can be improved in Gradle run: ./gradlew :mocks:publishToMavenCentral :annotation:publishToMavenCentral :annotation:compiler:publishToMavenCentral :library:publishToMavenCentral :integration:sqljournaldiskcache:publishToMavenCentral :annotation:ksp:publishToMavenCentral :integration:recyclerview:publishToMavenCentral :integration:avif:publishToMavenCentral :integration:okhttp:publishToMavenCentral :integration:gifencoder:publishToMavenCentral :integration:ktx:publishToMavenCentral :integration:okhttp4:publishToMavenCentral :integration:volley:publishToMavenCentral :integration:concurrent:publishToMavenCentral :integration:cronet:publishToMavenCentral :integration:okhttp3:publishToMavenCentral :integration:compose:publishToMavenCentral :third_party:disklrucache:publishToMavenCentral :third_party:gif_decoder:publishToMavenCentral env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.MAVEN_SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_PRIVATE_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PRIVATE_KEY_PASSWORD }} ORG_GRADLE_PROJECT_mavenCentralPublishing: true ORG_GRADLE_PROJECT_mavenCentralAutomaticPublishing: false ================================================ FILE: .gitignore ================================================ # Android local.properties *.keystore *.DS_Store # Gradle .gradle build jacoco.exec # gh-pages doc/** _site/* _pages/* docs/**/* # Vim *.swp *.swo # sed *.bak # Intellij *.iml *.ipr *.iws .idea/** !.idea/codeStyleSettings.xml !.idea/inspectionProfiles !.idea/inspectionProfiles/Project_Default.xml ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .idea/codeStyleSettings.xml ================================================ ================================================ FILE: .idea/inspectionProfiles/Project_Default.xml ================================================ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Contributions of all types are welcome. We use GitHub as our bug and feature tracker both for code and for other aspects of the library (documentation, the wiki, etc.). ## Asking Questions The best way to ask general questions is to send an email to our [mailing list][2], or join [#glide-library on freenode.org][3]. ## Filing issues When in doubt, file an issue. We'd rather close a few duplicate issues than let a problem go unnoticed. Similarly if you support a particular feature request, feel free to let us know by commenting on the issue or [subscribing][6] to the issue. To file a new issue, please use our issue template and fill out the template as much as possible (remove irrelevant parts). The more information you can provide, the more likely we are to be able help. ## Contributing code Pull requests are welcome for all parts of the codebase, especially the integration libraries. You can find instructions on building the project in [README.md][5]. Our code style is defined in Intellij project files in the repo and also by our Checkstyle config. If you'd like to submit code, but can't get the style checks to pass, feel free to put up your pull request anyway and we can help you fix the style issues. If you'd like to contribute code, you will need to sign [Google's individual contributor license agreement][4] which will be asked when you create the PR by [googlebot](https://github.com/googlebot) should you forget it. ## Labels Labels on issues are managed by contributors, you don't have to worry about them. Here's a list of what they mean: * **bug**: feature that should work, but doesn't * **enhancement**: minor tweak/addition to existing behavior * **feature**: new behavior, bigger than enhancement, it gives more bang to Glide * **question**: no need to modify Glide to fix the issue, usually a usage problem * **reproducible**: has enough information to very easily reproduce, mostly in form of a small project in a GitHub repo * **repro-needed**: we need some code to be able to reproduce and debug locally, otherwise there's not much we can do * **duplicate**: there's another issue which already covers/tracks this * **wontfix**: working as intended, or won't be fixed due to compatibility or other reasons * **invalid**: there isn't enough information to make a verdict, or unrelated to Glide * **non-library**: issue is not in the core library code, but rather in documentation, samples, build process, releases * **v4**: problem originated in v4, or question about v4 (while v3 is in wide use) *bug + enhancement: feature that doesn't work, but it's an edge case that either has a workaround or doesn't affect many users* [1]: https://github.com/bumptech/glide/issues/new?body=**Glide%20Version**%3A%0A**Integration%20libraries**%3A%0A**Device/Android%20Version**%3A%0A**Issue%20details%20/%20Repro%20steps%20/%20Use%20case%20background**%3A%0A%0A**Glide%20load%20line**%3A%0A%60%60%60java%0AGlide.with%28...%29.....load%28...%29.....into%28...%29%3B%0A%60%60%60%0A%0A**Layout%20XML**%3A%0A%60%60%60xml%0A%3C...Layout%3E%0A%20%20%20%20%3CImageView%20android%3AscaleType%3D%22...%22%20...%20/%3E%0A%3C/..Layout%3E%0A%60%60%60%0A%0A**Stack%20trace%20/%20LogCat**%3A%0A%60%60%60ruby%0Apaste%20stack%20trace%20here%0A%60%60%60 [2]: https://groups.google.com/forum/#!forum/glidelibrary [3]: http://webchat.freenode.net/?channels=glide-library [4]: https://developers.google.com/open-source/cla/individual [5]: https://github.com/bumptech/glide [6]: https://help.github.com/articles/subscribing-to-conversations/ ================================================ FILE: ISSUE_TEMPLATE.md ================================================ **Glide Version**: **Integration libraries**: **Device/Android Version**: **Issue details / Repro steps / Use case background**: **Glide load line / `GlideModule` (if any) / list Adapter code (if any)**: ```java Glide.with... ``` **Layout XML**: ```xml ## Description ## Motivation and Context ================================================ FILE: README.md ================================================ Glide ===== [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.bumptech.glide/glide/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.bumptech.glide/glide) | [View Glide's documentation][20] | [简体中文文档][22] | [Report an issue with Glide][5] Glide is a fast and efficient open source media management and image loading framework for Android that wraps media decoding, memory and disk caching, and resource pooling into a simple and easy to use interface. ![](static/glide_logo.png) Glide supports fetching, decoding, and displaying video stills, images, and animated GIFs. Glide includes a flexible API that allows developers to plug in to almost any network stack. By default Glide uses a custom `HttpUrlConnection` based stack, but also includes utility libraries plug in to Google's Volley project or Square's OkHttp library instead. Glide's primary focus is on making scrolling any kind of a list of images as smooth and fast as possible, but Glide is also effective for almost any case where you need to fetch, resize, and display a remote image. Download -------- For detailed instructions and requirements, see Glide's [download and setup docs page][28]. You can download a jar from GitHub's [releases page][1]. Or use Gradle: ```gradle repositories { google() mavenCentral() } dependencies { implementation 'com.github.bumptech.glide:glide:5.0.5' } ``` Or Maven: ```xml com.github.bumptech.glide glide 5.0.5 ``` For info on using the bleeding edge, see the [Snapshots][17] docs page. R8 / Proguard -------- The specific rules are [already bundled](library/proguard-rules.txt) into the aar which can be interpreted by R8 automatically How do I use Glide? ------------------- Check out the [documentation][20] for pages on a variety of topics, and see the [javadocs][3]. For Glide v3, see the [wiki][2]. Simple use cases will look something like this: ```java // For a simple view: @Override public void onCreate(Bundle savedInstanceState) { ... ImageView imageView = (ImageView) findViewById(R.id.my_image_view); Glide.with(this).load("https://goo.gl/gEgYUd").into(imageView); } // For a simple image list: @Override public View getView(int position, View recycled, ViewGroup container) { final ImageView myImageView; if (recycled == null) { myImageView = (ImageView) inflater.inflate(R.layout.my_image_view, container, false); } else { myImageView = (ImageView) recycled; } String url = myUrls.get(position); Glide .with(myFragment) .load(url) .centerCrop() .placeholder(R.drawable.loading_spinner) .into(myImageView); return myImageView; } ``` Status ------ Version 4 is now released and stable. Updates are released periodically with new features and bug fixes. Comments/bugs/questions/pull requests are always welcome! Please read [CONTRIBUTING.md][5] on how to report issues. Compatibility ------------- * **Minimum Android SDK**: Glide v4 requires a minimum API level of 14. * **Compile Android SDK**: Glide v4 requires you to compile against API 26 or later. If you need to support older versions of Android, consider staying on [Glide v3][14], which works on API 10, but is not actively maintained. * **OkHttp 3.x**: There is an optional dependency available called `okhttp3-integration`, see the [docs page][23]. * **Volley**: There is an optional dependency available called `volley-integration`, see the [docs page][24]. * **Round Pictures**: `CircleImageView`/`CircularImageView`/`RoundedImageView` are known to have [issues][18] with `TransitionDrawable` (`.crossFade()` with `.thumbnail()` or `.placeholder()`) and animated GIFs, use a [`BitmapTransformation`][19] (`.circleCrop()` will be available in v4) or `.dontAnimate()` to fix the issue. * **Huge Images** (maps, comic strips): Glide can load huge images by downsampling them, but does not support zooming and panning `ImageView`s as they require special resource optimizations (such as tiling) to work without `OutOfMemoryError`s. Build ----- Building Glide with gradle is fairly straight forward: ```shell git clone https://github.com/bumptech/glide.git cd glide ./gradlew jar ``` **Note**: Make sure your *Android SDK* has the *Android Support Repository* installed, and that your `$ANDROID_HOME` environment variable is pointing at the SDK or add a `local.properties` file in the root project with a `sdk.dir=...` line. Samples ------- Follow the steps in the [Build](#build) section to set up the project and then: ```shell ./gradlew :samples:flickr:run ./gradlew :samples:giphy:run ./gradlew :samples:svg:run ./gradlew :samples:contacturi:run ``` You may also find precompiled APKs on the [releases page][1]. Development ----------- Follow the steps in the [Build](#build) section to setup the project and then edit the files however you wish. [Android Studio][26] cleanly imports both Glide's source and tests and is the recommended way to work with Glide. To open the project in Android Studio: 1. Go to *File* menu or the *Welcome Screen* 2. Click on *Open...* 3. Navigate to Glide's root directory. 4. Select `setting.gradle` For more details, see the [Contributing docs page][27]. Getting Help ------------ To report a specific problem or feature request, [open a new issue on Github][5]. For questions, suggestions, or anything else, email [Glide's discussion group][6], or join our IRC channel: [irc.freenode.net#glide-library][13]. Contributing ------------ Before submitting pull requests, contributors must sign Google's [individual contributor license agreement][7]. Thanks ------ * The **Android team** and **Jake Wharton** for the [disk cache implementation][8] Glide's disk cache is based on. * **Dave Smith** for the [GIF decoder gist][9] Glide's GIF decoder is based on. * **Chris Banes** for his [gradle-mvn-push][10] script. * **Corey Hall** for Glide's [amazing logo][11]. * Everyone who has contributed code and reported issues! Author ------ Sam Judd - @sjudd on GitHub, @samajudd on Twitter License ------- BSD, part MIT and Apache 2.0. See the [LICENSE][16] file for details. Disclaimer --------- This is not an official Google product. [1]: https://github.com/bumptech/glide/releases [2]: https://github.com/bumptech/glide/wiki [3]: https://bumptech.github.io/glide/ref/javadocs.html [4]: https://www.jetbrains.com/idea/download/ [5]: https://github.com/bumptech/glide/blob/master/CONTRIBUTING.md [6]: https://groups.google.com/forum/#!forum/glidelibrary [7]: https://developers.google.com/open-source/cla/individual [8]: https://github.com/JakeWharton/DiskLruCache [9]: https://gist.github.com/devunwired/4479231 [10]: https://github.com/chrisbanes/gradle-mvn-push [11]: static/glide_logo.png [12]: https://github.com/bumptech/glide/wiki/Integration-Libraries [13]: http://webchat.freenode.net/?channels=glide-library [14]: https://github.com/bumptech/glide/tree/3.0 [15]: https://github.com/bumptech/glide/tree/master [16]: https://github.com/bumptech/glide/blob/master/LICENSE [17]: http://bumptech.github.io/glide/dev/snapshots.html [18]: https://github.com/bumptech/glide/issues?q=is%3Aissue+CircleImageView+OR+CircularImageView+OR+RoundedImageView [19]: https://github.com/wasabeef/glide-transformations [20]: https://bumptech.github.io/glide/ [22]: https://muyangmin.github.io/glide-docs-cn/ [23]: http://bumptech.github.io/glide/int/okhttp3.html [24]: http://bumptech.github.io/glide/int/volley.html [25]: http://bumptech.github.io/glide/doc/download-setup.html#proguard [26]: https://developer.android.com/studio/index.html [27]: http://bumptech.github.io/glide/dev/contributing.html [28]: http://bumptech.github.io/glide/doc/download-setup.html ================================================ FILE: annotation/.gitignore ================================================ /build ================================================ FILE: annotation/build.gradle.kts ================================================ plugins { id("java") } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") java { sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 } ================================================ FILE: annotation/compiler/.gitignore ================================================ /build ================================================ FILE: annotation/compiler/build.gradle.kts ================================================ plugins { id("java") } dependencies { implementation(libs.javapoet) implementation(libs.guava) compileOnly(libs.autoservice) compileOnly(libs.findbugs.jsr305) implementation(project(":annotation")) annotationProcessor(libs.autoservice) } tasks.withType { isFailOnError = false } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: annotation/compiler/gradle.properties ================================================ POM_NAME=Glide Annotation processor POM_ARTIFACT_ID=compiler POM_PACKAGING=jar POM_DESCRIPTION=Glide's anntation processor. Should be included in all Applications and in all libraries that use Glide's modules for configuration. ================================================ FILE: annotation/compiler/proguard.pro ================================================ -verbose # Use ProGuard only to get rid of unused classes -dontobfuscate -dontoptimize -keepattributes * -keep class !com.bumptech.glide.repackaged.**,com.bumptech.glide.** # Keep the entry point to this library, see META-INF\services\javax.annotation.processing.Processor -keep class com.bumptech.glide.annotation.compiler.GlideAnnotationProcessor # "duplicate definition of library class" -dontnote sun.applet.** # "duplicate definition of library class" -dontnote sun.tools.jar.** # Reflective accesses in com.google.common.util.concurrent.* and some others -dontnote com.bumptech.glide.repackaged.com.google.common.** # com.google.common.collect.* and some others (….common.*.*) -dontwarn com.google.j2objc.annotations.Weak # com.google.common.util.concurrent.FuturesGetChecked$GetCheckedTypeValidatorHolder$ClassValueValidator -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement #-dontwarn ** ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/AppModuleGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.Excludes; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeSpec.Builder; import com.squareup.javapoet.WildcardTypeName; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; /** * Generates a new implementation of a AppGlideModule that calls all included LibraryGlideModules * and the original AppGlideModule. * *

The generated class will always call the AppGlideModule last to give it priority over choices * made or classes registered in LibraryGlideModules. * *

Android logging is included to allow developers to see exactly which modules are included at * runtime. * *

The generated class looks something like this: * *

 * 
 *  final class GeneratedAppGlideModuleImpl extends com.bumptech.glide.GeneratedAppGlideModule {
 *    private final com.bumptech.glide.samples.giphy.GiphyGlideModule appGlideModule;
 *
 *    GeneratedAppGlideModule() {
 *      appGlideModule = new com.bumptech.glide.samples.giphy.GiphyGlideModule();
 *      if (android.util.Log.isLoggable("Glide", android.util.Log.DEBUG)) {
 *        android.util.Log.d("Glide", "Discovered AppGlideModule from annotation:"
 *            + " com.bumptech.glide.samples.giphy.GiphyGlideModule");
 *        android.util.Log.d("Glide", "Discovered LibraryGlideModule from annotation:"
 *            + "com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule");
 *      }
 *    }
 *
 *    {@literal @java.lang.Override}
 *    public void applyOptions(android.content.Context context,
 *        com.bumptech.glide.GlideBuilder builder) {
 *      appGlideModule.applyOptions(context, builder);
 *    }
 *
 *    {@literal @java.lang.Override}
 *    public void registerComponents(android.content.Context context,
 *        com.bumptech.glide.Registry registry) {
 *      new com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule()
 *          .registerComponents(context, registry);
 *      appGlideModule.registerComponents(context, registry);
 *    }
 *
 *    {@literal @java.lang.Override}
 *    public boolean isManifestParsingEnabled() {
 *      return appGlideModule.isManifestParsingEnabled();
 *    }
 *
 *    {@literal @java.lang.Override}
 *    {@literal @androidx.annotation.NonNull}
 *    public java.util.Set<java.lang.Class<?>> getExcludedModuleClasses() {
 *      return appGlideModule.getExcludedModuleClasses();
 *    }
 *  }
 * 
 * 
*/ final class AppModuleGenerator { static final String GENERATED_ROOT_MODULE_PACKAGE_NAME = "com.bumptech.glide"; private static final String GLIDE_LOG_TAG = "Glide"; private static final String GENERATED_APP_MODULE_IMPL_SIMPLE_NAME = "GeneratedAppGlideModuleImpl"; private static final String GENERATED_ROOT_MODULE_SIMPLE_NAME = "GeneratedAppGlideModule"; private final ProcessingEnvironment processingEnv; private final ProcessorUtil processorUtil; AppModuleGenerator(ProcessingEnvironment processingEnv, ProcessorUtil processorUtil) { this.processingEnv = processingEnv; this.processorUtil = processorUtil; } TypeSpec generate(TypeElement appGlideModule, Set libraryGlideModuleClassNames) { ClassName appGlideModuleClassName = ClassName.get(appGlideModule); List excludedGlideModuleClassNames = getExcludedGlideModuleClassNames(appGlideModule); List orderedLibraryGlideModuleClassNames = new ArrayList<>(libraryGlideModuleClassNames); Collections.sort(orderedLibraryGlideModuleClassNames); MethodSpec constructor = generateConstructor( appGlideModuleClassName, orderedLibraryGlideModuleClassNames, excludedGlideModuleClassNames); MethodSpec registerComponents = generateRegisterComponents( orderedLibraryGlideModuleClassNames, excludedGlideModuleClassNames); MethodSpec getExcludedModuleClasses = generateGetExcludedModuleClasses(excludedGlideModuleClassNames); MethodSpec applyOptions = MethodSpec.methodBuilder("applyOptions") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter( ParameterSpec.builder(ClassName.get("android.content", "Context"), "context") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder( ClassName.get("com.bumptech.glide", "GlideBuilder"), "builder") .addAnnotation(processorUtil.nonNull()) .build()) .addStatement("appGlideModule.applyOptions(context, builder)", appGlideModule) .build(); MethodSpec isManifestParsingEnabled = MethodSpec.methodBuilder("isManifestParsingEnabled") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(boolean.class) .addStatement("return appGlideModule.isManifestParsingEnabled()", appGlideModule) .build(); Builder builder = TypeSpec.classBuilder(GENERATED_APP_MODULE_IMPL_SIMPLE_NAME) .addModifiers(Modifier.FINAL) .addAnnotation( AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "deprecation") .build()) .superclass( ClassName.get( GENERATED_ROOT_MODULE_PACKAGE_NAME, GENERATED_ROOT_MODULE_SIMPLE_NAME)) .addField(appGlideModuleClassName, "appGlideModule", Modifier.PRIVATE, Modifier.FINAL) .addMethod(constructor) .addMethod(applyOptions) .addMethod(registerComponents) .addMethod(isManifestParsingEnabled) .addMethod(getExcludedModuleClasses); ClassName generatedRequestManagerFactoryClassName = ClassName.get( RequestManagerFactoryGenerator.GENERATED_REQUEST_MANAGER_FACTORY_PACKAGE_NAME, RequestManagerFactoryGenerator.GENERATED_REQUEST_MANAGER_FACTORY_SIMPLE_NAME); builder.addMethod( MethodSpec.methodBuilder("getRequestManagerFactory") .addAnnotation(Override.class) .addAnnotation(processorUtil.nonNull()) .returns(generatedRequestManagerFactoryClassName) .addStatement("return new $T()", generatedRequestManagerFactoryClassName) .build()); return builder.build(); } // TODO: When we drop support for parsing GlideModules from AndroidManifests, remove this method. private MethodSpec generateGetExcludedModuleClasses(Collection excludedClassNames) { TypeName wildCardOfObject = WildcardTypeName.subtypeOf(Object.class); ParameterizedTypeName classOfWildcardOfObjet = ParameterizedTypeName.get(ClassName.get(Class.class), wildCardOfObject); ParameterizedTypeName setOfClassOfWildcardOfObject = ParameterizedTypeName.get(ClassName.get(Set.class), classOfWildcardOfObjet); ParameterizedTypeName hashSetOfClassOfWildcardOfObject = ParameterizedTypeName.get(ClassName.get(HashSet.class), classOfWildcardOfObjet); MethodSpec.Builder builder = MethodSpec.methodBuilder("getExcludedModuleClasses") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addAnnotation(processorUtil.nonNull()) .returns(setOfClassOfWildcardOfObject); if (excludedClassNames.isEmpty()) { builder.addStatement("return $T.emptySet()", Collections.class); } else { builder.addStatement( "$T excludedClasses = new $T()", setOfClassOfWildcardOfObject, hashSetOfClassOfWildcardOfObject); for (String excludedClassName : excludedClassNames) { // TODO: Remove this when we no longer support manifest parsing. // Using a Literal ($L) instead of a type ($T) to get a fully qualified import that allows // us to suppress deprecation warnings. Aimed at deprecated GlideModules. builder.addStatement("excludedClasses.add($L.class)", excludedClassName); } builder.addStatement("return excludedClasses"); } return builder.build(); } private MethodSpec generateRegisterComponents( Collection libraryGlideModuleClassNames, Collection excludedGlideModuleClassNames) { MethodSpec.Builder registerComponents = MethodSpec.methodBuilder("registerComponents") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter( ParameterSpec.builder(ClassName.get("android.content", "Context"), "context") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(ClassName.get("com.bumptech.glide", "Glide"), "glide") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(ClassName.get("com.bumptech.glide", "Registry"), "registry") .addAnnotation(processorUtil.nonNull()) .build()); for (String glideModule : libraryGlideModuleClassNames) { if (excludedGlideModuleClassNames.contains(glideModule)) { continue; } ClassName moduleClassName = ClassName.bestGuess(glideModule); registerComponents.addStatement( "new $T().registerComponents(context, glide, registry)", moduleClassName); } // Order matters here. The AppGlideModule must be called last. registerComponents.addStatement("appGlideModule.registerComponents(context, glide, registry)"); return registerComponents.build(); } private boolean doesAppGlideModuleConstructorAcceptContext(ClassName appGlideModule) { TypeElement appGlideModuleType = processingEnv.getElementUtils().getTypeElement(appGlideModule.reflectionName()); for (Element enclosed : appGlideModuleType.getEnclosedElements()) { if (enclosed.getKind() == ElementKind.CONSTRUCTOR) { ExecutableElement constructor = (ExecutableElement) enclosed; List parameters = constructor.getParameters(); if (parameters.isEmpty()) { return false; } else if (parameters.size() > 1) { throw new IllegalStateException( "Constructor for " + appGlideModule + " accepts too many parameters" + ", it should accept no parameters, or a single Context"); } else { VariableElement parameter = parameters.get(0); TypeMirror parameterType = parameter.asType(); TypeMirror contextType = processingEnv.getElementUtils().getTypeElement("android.content.Context").asType(); if (!processingEnv.getTypeUtils().isSameType(parameterType, contextType)) { throw new IllegalStateException("Unrecognized type: " + parameterType); } return true; } } } return false; } private MethodSpec generateConstructor( ClassName appGlideModule, Collection libraryGlideModuleClassNames, Collection excludedGlideModuleClassNames) { MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter( ParameterSpec.builder(ClassName.get("android.content", "Context"), "context") .build()); if (doesAppGlideModuleConstructorAcceptContext(appGlideModule)) { constructorBuilder.addStatement("appGlideModule = new $T(context)", appGlideModule); } else { constructorBuilder.addStatement("appGlideModule = new $T()", appGlideModule); } ClassName androidLogName = ClassName.get("android.util", "Log"); // Add some log lines to indicate to developers which modules where discovered. constructorBuilder.beginControlFlow( "if ($T.isLoggable($S, $T.DEBUG))", androidLogName, GLIDE_LOG_TAG, androidLogName); constructorBuilder.addStatement( "$T.d($S, $S)", androidLogName, GLIDE_LOG_TAG, "Discovered AppGlideModule from annotation: " + appGlideModule); // Excluded GlideModule classes from the manifest are logged in Glide's singleton. for (String glideModule : libraryGlideModuleClassNames) { if (excludedGlideModuleClassNames.contains(glideModule)) { constructorBuilder.addStatement( "$T.d($S, $S)", androidLogName, GLIDE_LOG_TAG, "AppGlideModule excludes LibraryGlideModule from annotation: " + glideModule); } else { constructorBuilder.addStatement( "$T.d($S, $S)", androidLogName, GLIDE_LOG_TAG, "Discovered LibraryGlideModule from annotation: " + glideModule); } } constructorBuilder.endControlFlow(); return constructorBuilder.build(); } private List getExcludedGlideModuleClassNames(TypeElement appGlideModule) { Set names = processorUtil.findClassValuesFromAnnotationOnClassAsNames(appGlideModule, Excludes.class); List result = new ArrayList<>(names); Collections.sort(result); return result; } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/AppModuleProcessor.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideModule; import com.squareup.javapoet.TypeSpec; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; /** * Runs the final steps of Glide's annotation process and generates the combined {@code * AppGlideModule}, {@code com.bumptech.glide.Glide}, {@code com.bumptech.glide.RequestManager}, and * {@code com.bumptech.glide.request.RequestOptions} classes. */ final class AppModuleProcessor { private static final String COMPILER_PACKAGE_NAME = GlideAnnotationProcessor.class.getPackage().getName(); private final ProcessingEnvironment processingEnv; private final ProcessorUtil processorUtil; private final List appGlideModules = new ArrayList<>(); private final RequestOptionsGenerator requestOptionsGenerator; private final RequestManagerGenerator requestManagerGenerator; private final AppModuleGenerator appModuleGenerator; private final RequestBuilderGenerator requestBuilderGenerator; private final RequestManagerFactoryGenerator requestManagerFactoryGenerator; private final GlideGenerator glideGenerator; AppModuleProcessor(ProcessingEnvironment processingEnv, ProcessorUtil processorUtil) { this.processingEnv = processingEnv; this.processorUtil = processorUtil; appModuleGenerator = new AppModuleGenerator(processingEnv, processorUtil); requestOptionsGenerator = new RequestOptionsGenerator(processingEnv, processorUtil); requestManagerGenerator = new RequestManagerGenerator(processingEnv, processorUtil); requestManagerFactoryGenerator = new RequestManagerFactoryGenerator(processingEnv, processorUtil); glideGenerator = new GlideGenerator(processingEnv, processorUtil); requestBuilderGenerator = new RequestBuilderGenerator(processingEnv, processorUtil); } void processModules(Set set, RoundEnvironment env) { for (TypeElement element : processorUtil.getElementsFor(GlideModule.class, env)) { if (processorUtil.isAppGlideModule(element)) { appGlideModules.add(element); } } processorUtil.debugLog("got app modules: " + appGlideModules); if (appGlideModules.size() > 1) { throw new IllegalStateException( "You cannot have more than one AppGlideModule, found: " + appGlideModules); } } boolean maybeWriteAppModule() { // appGlideModules is added to in order to catch errors where multiple AppGlideModules may be // present for a single application or library. Because we only add to appGlideModules, we use // isGeneratedAppGlideModuleWritten to make sure the GeneratedAppGlideModule is written at // most once. if (appGlideModules.isEmpty()) { return false; } TypeElement appModule = appGlideModules.get(0); processorUtil.debugLog("Processing app module: " + appModule); // If this package is null, it means there are no classes with this package name. One way this // could happen is if we process an annotation and reach this point without writing something // to the package. We do not error check here because that shouldn't happen with the // current implementation. PackageElement glideGenPackage = processingEnv.getElementUtils().getPackageElement(COMPILER_PACKAGE_NAME); FoundIndexedClassNames indexedClassNames = getIndexedClassNames(glideGenPackage); // Write all generated code to the package containing the AppGlideModule. Doing so fixes // classpath collisions if more than one Application containing a AppGlideModule is included // in a project. String generatedCodePackageName = appModule.getEnclosingElement().toString(); TypeSpec generatedRequestOptions = requestOptionsGenerator.generate(generatedCodePackageName, indexedClassNames.extensions); writeRequestOptions(generatedCodePackageName, generatedRequestOptions); TypeSpec generatedRequestBuilder = requestBuilderGenerator.generate( generatedCodePackageName, indexedClassNames.extensions, generatedRequestOptions); writeRequestBuilder(generatedCodePackageName, generatedRequestBuilder); TypeSpec requestManager = requestManagerGenerator.generate( generatedCodePackageName, generatedRequestOptions, generatedRequestBuilder, indexedClassNames.extensions); writeRequestManager(generatedCodePackageName, requestManager); TypeSpec requestManagerFactory = requestManagerFactoryGenerator.generate(generatedCodePackageName, requestManager); writeRequestManagerFactory(requestManagerFactory); TypeSpec glide = glideGenerator.generate(generatedCodePackageName, getGlideName(appModule), requestManager); writeGlide(generatedCodePackageName, glide); TypeSpec generatedAppGlideModule = appModuleGenerator.generate(appModule, indexedClassNames.glideModules); writeAppModule(generatedAppGlideModule); processorUtil.infoLog("Wrote GeneratedAppGlideModule with: " + indexedClassNames.glideModules); return true; } private String getGlideName(TypeElement appModule) { return appModule.getAnnotation(GlideModule.class).glideName(); } @SuppressWarnings("unchecked") private FoundIndexedClassNames getIndexedClassNames(PackageElement glideGenPackage) { Set glideModules = new HashSet<>(); Set extensions = new HashSet<>(); List glideGeneratedElements = glideGenPackage.getEnclosedElements(); for (Element indexer : glideGeneratedElements) { Index annotation = indexer.getAnnotation(Index.class); // If the annotation is null, it means we've come across another class in the same package // that we can safely ignore. if (annotation != null) { Collections.addAll(glideModules, annotation.modules()); Collections.addAll(extensions, annotation.extensions()); } } processorUtil.debugLog("Found GlideModules: " + glideModules); return new FoundIndexedClassNames(glideModules, extensions); } private void writeGlide(String packageName, TypeSpec glide) { processorUtil.writeClass(packageName, glide); } private void writeRequestManager(String packageName, TypeSpec requestManager) { processorUtil.writeClass(packageName, requestManager); } // We dont' care about collisions in IDEs since this class isn't an API class. private void writeRequestManagerFactory(TypeSpec requestManagerFactory) { processorUtil.writeClass( AppModuleGenerator.GENERATED_ROOT_MODULE_PACKAGE_NAME, requestManagerFactory); } // The app module we generate subclasses a package private class. We don't care about classpath // collisions in IDEs since this class isn't an API class. private void writeAppModule(TypeSpec appModule) { processorUtil.writeClass(AppModuleGenerator.GENERATED_ROOT_MODULE_PACKAGE_NAME, appModule); } private void writeRequestOptions(String packageName, TypeSpec requestOptions) { processorUtil.writeClass(packageName, requestOptions); } private void writeRequestBuilder(String packageName, TypeSpec requestBuilder) { processorUtil.writeClass(packageName, requestBuilder); } private static final class FoundIndexedClassNames { private final Set glideModules; private final Set extensions; private FoundIndexedClassNames(Set glideModules, Set extensions) { this.glideModules = glideModules; this.extensions = extensions; } } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/ExtensionProcessor.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideExtension; import com.squareup.javapoet.TypeSpec; import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.TypeElement; /** * Writes Indexer classes annotated with {@link Index} for all classes found annotated with {@link * GlideExtension}. */ final class ExtensionProcessor { private final ProcessorUtil processorUtil; private final IndexerGenerator indexerGenerator; private final GlideExtensionValidator extensionValidator; ExtensionProcessor( ProcessingEnvironment processingEnvironment, ProcessorUtil processorUtil, IndexerGenerator indexerGenerator) { this.processorUtil = processorUtil; this.indexerGenerator = indexerGenerator; extensionValidator = new GlideExtensionValidator(processingEnvironment, processorUtil); } boolean processExtensions(RoundEnvironment env) { List elements = processorUtil.getElementsFor(GlideExtension.class, env); processorUtil.debugLog("Processing types : " + elements); for (TypeElement typeElement : elements) { extensionValidator.validateExtension(typeElement); processorUtil.debugLog("Processing elements: " + typeElement.getEnclosedElements()); } if (elements.isEmpty()) { return false; } TypeSpec spec = indexerGenerator.generate(elements); processorUtil.writeIndexer(spec); return true; } Set getSupportedAnnotationTypes() { return Collections.singleton(GlideExtension.class.getName()); } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/GlideAnnotationProcessor.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideType; import com.google.auto.service.AutoService; import java.util.HashSet; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; // Links in Javadoc will work due to build setup, even though there is no direct dependency here. /** * Generates classes based on Glide's annotations that configure Glide, add support for additional * resource types, and/or extend Glide's API. * *

This processor discovers all {@code AppGlideModule} and {@code LibraryGlideModule} * implementations that are annotated with {@link com.bumptech.glide.annotation.GlideModule}. Any * implementations missing the annotation will be ignored. * *

This processor also discovers all {@link com.bumptech.glide.annotation.GlideExtension} * annotated classes. * *

Multiple classes are generated by this processor: * *

    *
  • For {@code LibraryGlideModule}s - A GlideIndexer class in a specific package that will * later be used by the processor to discover all {@code LibraryGlideModule} classes. *
  • For {@code AppGlideModule}s - A single {@code AppGlideModule} implementation ({@code * com.bumptech.glide.GeneratedAppGlideModule}) that calls all {@code LibraryGlideModule}s and * the original {@code AppGlideModule} in the correct order when Glide is initialized. *
  • {@link com.bumptech.glide.annotation.GlideExtension}s - *
      *
    • A {@code com.bumptech.glide.request.RequestOptions} implementation that contains * static versions of all builder methods in the base class and both static and instance * versions of methods in all {@link com.bumptech.glide.annotation.GlideExtension}s. *
    • If one or more methods in one or more {@link * com.bumptech.glide.annotation.GlideExtension} annotated classes are annotated with * {@link GlideType}: *
        *
      • A {@code com.bumptech.glide.RequestManager} implementation containing a * generated method for each method annotated with {@link GlideType}. *
      • A {@code * com.bumptech.glide.manager.RequestManagerRetriever.RequestManagerFactory} * implementation that produces the generated {@code * com.bumptech.glide.RequestManager}s. *
      • A {@code com.bumptech.glide.Glide} look-alike that implements all static * methods in the {@code com.bumptech.glide.Glide} singleton and returns the * generated {@code com.bumptech.glide.RequestManager} implementation when * appropriate. *
      *
    *
* *

{@code AppGlideModule} implementations must only be included in applications, not in * libraries. There must be exactly one {@code AppGlideModule} implementation per Application. The * {@code AppGlideModule} class is used as a signal that all modules have been found and that the * final merged {@code com.bumptech.glide.GeneratedAppGlideModule} impl can be created. */ @AutoService(Processor.class) public final class GlideAnnotationProcessor extends AbstractProcessor { static final boolean DEBUG = false; private ProcessorUtil processorUtil; private LibraryModuleProcessor libraryModuleProcessor; private AppModuleProcessor appModuleProcessor; private boolean isGeneratedAppGlideModuleWritten; private ExtensionProcessor extensionProcessor; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); processorUtil = new ProcessorUtil(processingEnvironment); IndexerGenerator indexerGenerator = new IndexerGenerator(processorUtil); libraryModuleProcessor = new LibraryModuleProcessor(processorUtil, indexerGenerator); appModuleProcessor = new AppModuleProcessor(processingEnvironment, processorUtil); extensionProcessor = new ExtensionProcessor(processingEnvironment, processorUtil, indexerGenerator); } @Override public Set getSupportedAnnotationTypes() { Set result = new HashSet<>(); result.addAll(libraryModuleProcessor.getSupportedAnnotationTypes()); result.addAll(extensionProcessor.getSupportedAnnotationTypes()); return result; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * Each round we do the following: * *

    *
  1. Find all {@code AppGlideModule}s and save them to an instance variable (throw if > 1). *
  2. Find all {@code LibraryGlideModule}s *
  3. For each {@code LibraryGlideModule}, write an {@code Indexer} with an Annotation with the * class name. *
  4. If we wrote any {@code Indexer}s, return and wait for the next round. *
  5. If we didn't write any {@code Indexer}s and there is a {@code AppGlideModule}, write the * {@code GeneratedAppGlideModule}. Once the {@code GeneratedAppGlideModule} is written, we * expect to be finished. Any further generation of related classes will result in errors. *
*/ @Override public boolean process(Set set, RoundEnvironment env) { processorUtil.process(); boolean newModulesWritten = libraryModuleProcessor.processModules(env); boolean newExtensionWritten = extensionProcessor.processExtensions(env); appModuleProcessor.processModules(set, env); if (newExtensionWritten || newModulesWritten) { if (isGeneratedAppGlideModuleWritten) { throw new IllegalStateException("Cannot process annotations after writing AppGlideModule"); } return false; } if (!isGeneratedAppGlideModuleWritten) { isGeneratedAppGlideModuleWritten = appModuleProcessor.maybeWriteAppModule(); } return false; } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/GlideExtensionValidator.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.ProcessorUtil.nonNulls; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.annotation.GlideType; import com.google.common.base.Function; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic.Kind; /** * Validates that classes annotated with {@link com.bumptech.glide.annotation.GlideExtension} * contains methods with the expected format. * *

Validation is performed so that errors can be found when a library is compiled. Without * validation, an error written in to a library wouldn't be found until Glide tried to generate code * for an Application. */ final class GlideExtensionValidator { private final ProcessingEnvironment processingEnvironment; private final ProcessorUtil processorUtil; GlideExtensionValidator( ProcessingEnvironment processingEnvironment, ProcessorUtil processorUtil) { this.processingEnvironment = processingEnvironment; this.processorUtil = processorUtil; } void validateExtension(TypeElement typeElement) { if (!typeElement.getModifiers().contains(Modifier.PUBLIC)) { throw new IllegalArgumentException( "RequestOptionsExtensions must be public, including: " + getName(typeElement)); } for (Element element : typeElement.getEnclosedElements()) { if (element.getKind() == ElementKind.CONSTRUCTOR) { validateExtensionConstructor(element); } else if (element.getKind() == ElementKind.METHOD) { ExecutableElement executableElement = (ExecutableElement) element; if (executableElement.getAnnotation(GlideOption.class) != null) { validateGlideOption(executableElement); } else if (executableElement.getAnnotation(GlideType.class) != null) { validateGlideType(executableElement); } } } } private static String getQualifiedMethodName(ExecutableElement executableElement) { return getEnclosingClassName(executableElement) + "#" + getName(executableElement); } private static String getEnclosingClassName(Element element) { return element.getEnclosingElement().toString(); } private static String getName(Element element) { return element.toString(); } private static void validateExtensionConstructor(Element element) { if (!element.getModifiers().contains(Modifier.PRIVATE)) { throw new IllegalArgumentException( "RequestOptionsExtensions must be public, with private constructors and only static" + " methods. Found a non-private constructor in: " + getEnclosingClassName(element)); } ExecutableElement executableElement = (ExecutableElement) element; if (!executableElement.getParameters().isEmpty()) { throw new IllegalArgumentException( "RequestOptionsExtensions must be public, with private constructors and only static" + " methods. Found parameters in the constructor of: " + getEnclosingClassName(element)); } } private void validateGlideOption(ExecutableElement executableElement) { validateGlideOptionAnnotations(executableElement); validateGlideOptionParameters(executableElement); TypeMirror returnType = executableElement.getReturnType(); if (!isBaseRequestOptions(returnType)) { throw new IllegalArgumentException( "@GlideOption methods should return a" + " BaseRequestOptions object, but " + getQualifiedMethodName(executableElement) + " returns " + returnType + ". If you're using old style @GlideOption methods, your" + " method may have a void return type, but doing so is deprecated and support will" + " be removed in a future version"); } validateGlideOptionOverride(executableElement); } private void validateGlideOptionAnnotations(ExecutableElement executableElement) { validateAnnotatedNonNull(executableElement); } private static void validateGlideOptionParameters(ExecutableElement executableElement) { if (executableElement.getParameters().isEmpty()) { throw new IllegalArgumentException( "@GlideOption methods must take a " + "BaseRequestOptions object as their first parameter, but " + getQualifiedMethodName(executableElement) + " has none"); } VariableElement first = executableElement.getParameters().get(0); TypeMirror expected = first.asType(); if (!isBaseRequestOptions(expected)) { throw new IllegalArgumentException( "@GlideOption methods must take a" + " BaseRequestOptions object as their first parameter, but the first parameter" + " in " + getQualifiedMethodName(executableElement) + " is " + expected); } } private static boolean isBaseRequestOptions(TypeMirror typeMirror) { return typeMirror.toString().equals("com.bumptech.glide.request.BaseRequestOptions"); } private void validateGlideOptionOverride(ExecutableElement element) { int overrideType = processorUtil.getOverrideType(element); boolean isOverridingBaseRequestOptionsMethod = isMethodInBaseRequestOptions(element); if (isOverridingBaseRequestOptionsMethod && overrideType == GlideOption.OVERRIDE_NONE) { throw new IllegalArgumentException( "Accidentally attempting to override a method in" + " BaseRequestOptions. Add an 'override' value in the @GlideOption annotation" + " if this is intentional. Offending method: " + getQualifiedMethodName(element)); } else if (!isOverridingBaseRequestOptionsMethod && overrideType != GlideOption.OVERRIDE_NONE) { throw new IllegalArgumentException( "Requested to override an existing method in" + " BaseRequestOptions, but no such method was found. Offending method: " + getQualifiedMethodName(element)); } } private boolean isMethodInBaseRequestOptions(ExecutableElement toFind) { // toFind is a method in a GlideExtension whose first argument is a BaseRequestOptions type. // Since we're comparing against methods in BaseRequestOptions itself, we need to drop that // first type. TypeElement requestOptionsType = processingEnvironment .getElementUtils() .getTypeElement(RequestOptionsGenerator.BASE_REQUEST_OPTIONS_QUALIFIED_NAME); List toFindParameterNames = getComparableParameterNames(toFind, true /*skipFirst*/); String toFindSimpleName = toFind.getSimpleName().toString(); for (Element element : requestOptionsType.getEnclosedElements()) { if (element.getKind() != ElementKind.METHOD) { continue; } ExecutableElement inBase = (ExecutableElement) element; if (toFindSimpleName.equals(inBase.getSimpleName().toString())) { List parameterNamesInBase = getComparableParameterNames(inBase, false /*skipFirst*/); if (parameterNamesInBase.equals(toFindParameterNames)) { return true; } } } return false; } private static List getComparableParameterNames( ExecutableElement element, boolean skipFirst) { List parameters = element.getParameters(); if (skipFirst) { parameters = parameters.subList(1, parameters.size()); } List result = new ArrayList<>(parameters.size()); for (VariableElement parameter : parameters) { result.add(parameter.asType().toString()); } return result; } private void validateGlideType(ExecutableElement executableElement) { TypeMirror returnType = executableElement.getReturnType(); validateGlideTypeAnnotations(executableElement); if (!isRequestBuilder(returnType) || !typeMatchesExpected(returnType, executableElement)) { String expectedClassName = getGlideTypeValue(executableElement); throw new IllegalArgumentException( "@GlideType methods should return a RequestBuilder<" + expectedClassName + "> object, but " + getQualifiedMethodName(executableElement) + " returns: " + returnType + ". If you're using old style @GlideType methods, your" + " method may have a void return type, but doing so is deprecated and support will" + " be removed in a future version"); } validateGlideTypeParameters(executableElement); } private String getGlideTypeValue(ExecutableElement executableElement) { return processorUtil .findClassValuesFromAnnotationOnClassAsNames(executableElement, GlideType.class) .iterator() .next(); } private boolean typeMatchesExpected(TypeMirror returnType, ExecutableElement executableElement) { if (!(returnType instanceof DeclaredType)) { return false; } List typeArguments = ((DeclaredType) returnType).getTypeArguments(); if (typeArguments.size() != 1) { return false; } TypeMirror argument = typeArguments.get(0); String expected = getGlideTypeValue(executableElement); return argument.toString().equals(expected); } private boolean isRequestBuilder(TypeMirror typeMirror) { TypeMirror toCompare = processingEnvironment.getTypeUtils().erasure(typeMirror); return toCompare.toString().equals("com.bumptech.glide.RequestBuilder"); } private static void validateGlideTypeParameters(ExecutableElement executableElement) { if (executableElement.getParameters().size() != 1) { throw new IllegalArgumentException( "@GlideType methods must take a" + " RequestBuilder object as their first and only parameter, but given multiple for: " + getQualifiedMethodName(executableElement)); } VariableElement first = executableElement.getParameters().get(0); TypeMirror argumentType = first.asType(); if (!argumentType.toString().startsWith("com.bumptech.glide.RequestBuilder")) { throw new IllegalArgumentException( "@GlideType methods must take a" + " RequestBuilder object as their first and only parameter, but given: " + argumentType + " for: " + getQualifiedMethodName(executableElement)); } } private void validateGlideTypeAnnotations(ExecutableElement executableElement) { validateAnnotatedNonNull(executableElement); } private void validateAnnotatedNonNull(ExecutableElement executableElement) { Set annotationNames = FluentIterable.from(executableElement.getAnnotationMirrors()) .transform( new Function() { @Override public String apply(AnnotationMirror input) { return input.getAnnotationType().asElement().toString(); } }) .toSet(); boolean noNonNull = true; for (ClassName nonNull : nonNulls()) { if (annotationNames.contains(nonNull.reflectionName())) { noNonNull = false; break; } } if (noNonNull) { processingEnvironment .getMessager() .printMessage( Kind.WARNING, getQualifiedMethodName(executableElement) + " is missing the " + processorUtil.nonNull().reflectionName() + " annotation," + " please add it to ensure that your extension methods are always returning" + " non-null values"); } } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/GlideGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideExtension; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.MethodSpec.Builder; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeSpec; import java.util.ArrayList; import java.util.List; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; /** * Generates a Glide look-alike that acts as the entry point to the generated API * (GlideApp.with(...)). * *

Generated {@code com.bumptech.glide.Glide} look-alikes look like this (note that the name is * configurable in {@link com.bumptech.glide.annotation.GlideModule}): * *

 * 
 * public final class GlideApp {
 *   private GiphyGlide() {
 *   }
 *
 *   public static File getPhotoCacheDir(Context context) {
 *     return Glide.getPhotoCacheDir(context);
 *   }
 *
 *   public static File getPhotoCacheDir(Context context, String cacheName) {
 *     return Glide.getPhotoCacheDir(context, cacheName);
 *   }
 *
 *   public static Glide get(Context context) {
 *     return Glide.get(context);
 *   }
 *
 *   public static void tearDown() {
 *     Glide.tearDown();
 *   }
 *
 *   public static GeneratedRequestManager with(Context context) {
 *     return (GeneratedRequestManager) Glide.with(context);
 *   }
 *
 *   public static GeneratedRequestManager with(Activity activity) {
 *    return (GeneratedRequestManager) Glide.with(activity);
 *   }
 *
 *   public static GeneratedRequestManager with(FragmentActivity activity) {
 *     return (GeneratedRequestManager) Glide.with(activity);
 *   }
 *
 *   public static GeneratedRequestManager with(Fragment fragment) {
 *     return (GeneratedRequestManager) Glide.with(fragment);
 *   }
 *
 *   public static GeneratedRequestManager with(androidx.fragment.app.Fragment fragment) {
 *     return (GeneratedRequestManager) Glide.with(fragment);
 *   }
 * 
 * 
*/ final class GlideGenerator { private static final String GLIDE_QUALIFIED_NAME = "com.bumptech.glide.Glide"; private static final String REQUEST_MANAGER_QUALIFIED_NAME = "com.bumptech.glide.RequestManager"; private static final String SUPPRESS_LINT_PACKAGE_NAME = "android.annotation"; private static final String SUPPRESS_LINT_CLASS_NAME = "SuppressLint"; private final ProcessingEnvironment processingEnv; private final ProcessorUtil processorUtil; private final TypeElement glideType; private final TypeElement requestManagerType; GlideGenerator(ProcessingEnvironment processingEnv, ProcessorUtil processorUtil) { this.processingEnv = processingEnv; this.processorUtil = processorUtil; Elements elementUtils = processingEnv.getElementUtils(); requestManagerType = elementUtils.getTypeElement(REQUEST_MANAGER_QUALIFIED_NAME); glideType = elementUtils.getTypeElement(GLIDE_QUALIFIED_NAME); } TypeSpec generate( String generatedCodePackageName, String glideName, TypeSpec generatedRequestManager) { return TypeSpec.classBuilder(glideName) .addJavadoc( "The entry point for interacting with Glide for Applications\n" + "\n" + "

Includes all generated APIs from all\n" + "{@link $T}s in source and dependent libraries.\n" + "\n" + "

This class is generated and should not be modified" + "\n" + "@see $T\n", GlideExtension.class, glideType) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build()) .addMethods( generateOverridesForGlideMethods(generatedCodePackageName, generatedRequestManager)) .build(); } private List generateOverridesForGlideMethods( final String generatedCodePackageName, final TypeSpec generatedRequestManager) { return Lists.transform( discoverGlideMethodsToOverride(), new Function() { @Override public MethodSpec apply(ExecutableElement input) { if (isGlideWithMethod(input)) { return overrideGlideWithMethod( generatedCodePackageName, generatedRequestManager, input); } else { return overrideGlideStaticMethod(input); } } }); } private MethodSpec overrideGlideStaticMethod(ExecutableElement methodToOverride) { List parameters = processorUtil.getParameters(methodToOverride); TypeElement element = (TypeElement) processingEnv.getTypeUtils().asElement(methodToOverride.getReturnType()); MethodSpec.Builder builder = MethodSpec.methodBuilder(methodToOverride.getSimpleName().toString()) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addJavadoc(processorUtil.generateSeeMethodJavadoc(methodToOverride)) .addParameters(parameters); addReturnAnnotations(builder, methodToOverride); boolean returnsValue = element != null; if (returnsValue) { builder.returns(ClassName.get(element)); } StringBuilder code = new StringBuilder(returnsValue ? "return " : ""); code.append("$T.$N("); List args = new ArrayList<>(); args.add(ClassName.get(glideType)); args.add(methodToOverride.getSimpleName()); if (!parameters.isEmpty()) { for (ParameterSpec param : parameters) { code.append("$L, "); args.add(param.name); } code = new StringBuilder(code.substring(0, code.length() - 2)); } code.append(")"); builder.addStatement(code.toString(), args.toArray(new Object[0])); return builder.build(); } private Builder addReturnAnnotations(Builder builder, ExecutableElement methodToOverride) { Elements elements = processingEnv.getElementUtils(); TypeElement visibleForTestingTypeElement = elements.getTypeElement(processorUtil.visibleForTesting().reflectionName()); String visibleForTestingTypeQualifiedName = visibleForTestingTypeElement.toString(); for (AnnotationMirror mirror : methodToOverride.getAnnotationMirrors()) { builder.addAnnotation(AnnotationSpec.get(mirror)); // Suppress a lint warning if we're overriding a VisibleForTesting method. // See #1977. String annotationQualifiedName = mirror.getAnnotationType().toString(); if (annotationQualifiedName.equals(visibleForTestingTypeQualifiedName)) { builder.addAnnotation( AnnotationSpec.builder( ClassName.get(SUPPRESS_LINT_PACKAGE_NAME, SUPPRESS_LINT_CLASS_NAME)) .addMember("value", "$S", "VisibleForTests") .build()); } } return builder; } private List discoverGlideMethodsToOverride() { return processorUtil.findStaticMethods(glideType); } private boolean isGlideWithMethod(ExecutableElement element) { return processorUtil.isReturnValueTypeMatching(element, requestManagerType); } private MethodSpec overrideGlideWithMethod( String packageName, TypeSpec generatedRequestManager, ExecutableElement methodToOverride) { ClassName generatedRequestManagerClassName = ClassName.get(packageName, generatedRequestManager.name); List parameters = processorUtil.getParameters(methodToOverride); Preconditions.checkArgument( parameters.size() == 1, "Expected size of 1, but got %s", methodToOverride); ParameterSpec parameter = parameters.iterator().next(); Builder builder = MethodSpec.methodBuilder(methodToOverride.getSimpleName().toString()) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addJavadoc(processorUtil.generateSeeMethodJavadoc(methodToOverride)) .addParameters(parameters) .returns(generatedRequestManagerClassName) .addStatement( "return ($T) $T.$N($L)", generatedRequestManagerClassName, glideType, methodToOverride.getSimpleName().toString(), parameter.name); return addReturnAnnotations(builder, methodToOverride).build(); } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/IndexerGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideModule; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeSpec; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; import java.util.UUID; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; /** * Generates an empty class with an annotation containing the class names of one or more * LibraryGlideModules and/or one or more GlideExtensions. * *

We use a separate class so that LibraryGlideModules and GlideExtensions written in libraries * can be bundled into an AAR and later retrieved by the annotation processor when it processes the * AppGlideModule in an application. * *

The output file generated by this class with a LibraryGlideModule looks like this: * *

 * 
 *  {@literal @com.bumptech.glide.annotation.compiler.Index(}
 *      modules = "com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule"
 *  )
 *  public class Indexer_GlideModule_com_bumptech_glide_integration_okhttp3_OkHttpLibraryGlideModule
 *  {
 *  }
 * 
 * 
* *

The output file generated by this class with a GlideExtension looks like this: * *

 * 
 *  {@literal @com.bumptech.glide.annotation.compiler.Index(}
 *      extensions = "com.bumptech.glide.integration.gif.GifOptions"
 *  )
 *  public class Indexer_GlideExtension_com_bumptech_glide_integration_gif_GifOptions {
 *  }
 * 
 * 
*/ final class IndexerGenerator { private static final String INDEXER_NAME_PREFIX = "GlideIndexer_"; private static final int MAXIMUM_FILE_NAME_LENGTH = 255; private final ProcessorUtil processorUtil; IndexerGenerator(ProcessorUtil processorUtil) { this.processorUtil = processorUtil; } TypeSpec generate(List types) { List modules = new ArrayList<>(); List extensions = new ArrayList<>(); for (TypeElement element : types) { if (processorUtil.isExtension(element)) { extensions.add(element); } else if (processorUtil.isLibraryGlideModule(element)) { modules.add(element); } else { throw new IllegalArgumentException("Unrecognized type: " + element); } } if (!modules.isEmpty() && !extensions.isEmpty()) { throw new IllegalArgumentException( "Given both modules and extensions, expected one or the " + "other. Modules: " + modules + " Extensions: " + extensions); } if (!modules.isEmpty()) { return generate(types, GlideModule.class); } else { return generate(types, GlideExtension.class); } } private TypeSpec generate( List libraryModules, Class annotation) { AnnotationSpec.Builder annotationBuilder = AnnotationSpec.builder(Index.class); String value = getAnnotationValue(annotation); for (TypeElement childModule : libraryModules) { annotationBuilder.addMember(value, "$S", ClassName.get(childModule).toString()); } StringBuilder indexerNameBuilder = new StringBuilder(INDEXER_NAME_PREFIX + annotation.getSimpleName() + "_"); for (TypeElement element : libraryModules) { indexerNameBuilder.append(element.getQualifiedName().toString().replace(".", "_")); indexerNameBuilder.append("_"); } indexerNameBuilder = new StringBuilder(indexerNameBuilder.substring(0, indexerNameBuilder.length() - 1)); String indexerName = indexerNameBuilder.toString(); // If the indexer name has too many packages/modules, it can exceed the file name length // allowed by the file system, which can break compilation. To avoid that, fall back to a // deterministic UUID. if (indexerName.length() >= (MAXIMUM_FILE_NAME_LENGTH - INDEXER_NAME_PREFIX.length())) { indexerName = INDEXER_NAME_PREFIX + UUID.nameUUIDFromBytes(indexerName.getBytes()).toString().replace("-", "_"); } return TypeSpec.classBuilder(indexerName) .addAnnotation(annotationBuilder.build()) .addModifiers(Modifier.PUBLIC) .build(); } private static String getAnnotationValue(Class annotation) { if (annotation == GlideModule.class) { return "modules"; } else if (annotation == GlideExtension.class) { return "extensions"; } else { throw new IllegalArgumentException("Unrecognized annotation: " + annotation); } } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/LibraryModuleProcessor.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideModule; import com.squareup.javapoet.TypeSpec; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.TypeElement; /** Generates Indexer classes annotated with {@link Index} for all {@code LibraryGlideModule}s. */ final class LibraryModuleProcessor { private final ProcessorUtil processorUtil; private final IndexerGenerator indexerGenerator; LibraryModuleProcessor(ProcessorUtil processorUtil, IndexerGenerator indexerGenerator) { this.processorUtil = processorUtil; this.indexerGenerator = indexerGenerator; } boolean processModules(RoundEnvironment env) { // Order matters here, if we find an Indexer below, we return before writing the root module. // If we fail to add to appModules before then, we might accidentally skip a valid RootModule. List libraryGlideModules = new ArrayList<>(); for (TypeElement element : processorUtil.getElementsFor(GlideModule.class, env)) { // Root elements are added separately and must be checked separately because they're sub // classes of LibraryGlideModules. if (processorUtil.isAppGlideModule(element)) { continue; } else if (!processorUtil.isLibraryGlideModule(element)) { throw new IllegalStateException( "@GlideModule can only be applied to LibraryGlideModule" + " and AppGlideModule implementations, not: " + element); } libraryGlideModules.add(element); } processorUtil.debugLog("got child modules: " + libraryGlideModules); if (libraryGlideModules.isEmpty()) { return false; } TypeSpec indexer = indexerGenerator.generate(libraryGlideModules); processorUtil.writeIndexer(indexer); processorUtil.debugLog( "Wrote an Indexer this round, skipping the app module to ensure all " + "indexers are found"); // If I write an Indexer in a round in the target package, then try to find all classes in // the target package, my newly written Indexer won't be found. Since we wrote a class with // an Annotation handled by this processor, we know we will be called again in the next round // and we can safely wait to write our AppGlideModule until then. return true; } Set getSupportedAnnotationTypes() { return Collections.singleton(GlideModule.class.getName()); } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/ProcessorUtil.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.GlideAnnotationProcessor.DEBUG; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; /** Utilities for writing classes and logging. */ final class ProcessorUtil { private static final String GLIDE_MODULE_PACKAGE_NAME = "com.bumptech.glide.module"; private static final String APP_GLIDE_MODULE_SIMPLE_NAME = "AppGlideModule"; private static final String LIBRARY_GLIDE_MODULE_SIMPLE_NAME = "LibraryGlideModule"; private static final String APP_GLIDE_MODULE_QUALIFIED_NAME = GLIDE_MODULE_PACKAGE_NAME + "." + APP_GLIDE_MODULE_SIMPLE_NAME; private static final String LIBRARY_GLIDE_MODULE_QUALIFIED_NAME = GLIDE_MODULE_PACKAGE_NAME + "." + LIBRARY_GLIDE_MODULE_SIMPLE_NAME; private static final String COMPILER_PACKAGE_NAME = GlideAnnotationProcessor.class.getPackage().getName(); private static final ClassName SUPPORT_NONNULL_ANNOTATION = ClassName.get("android.support.annotation", "NonNull"); private static final ClassName JETBRAINS_NOTNULL_ANNOTATION = ClassName.get("org.jetbrains.annotations", "NotNull"); private static final ClassName ANDROIDX_NONNULL_ANNOTATION = ClassName.get("androidx.annotation", "NonNull"); private static final ClassName SUPPORT_CHECK_RESULT_ANNOTATION = ClassName.get("android.support.annotation", "CheckResult"); private static final ClassName ANDROIDX_CHECK_RESULT_ANNOTATION = ClassName.get("androidx.annotation", "CheckResult"); private static final ClassName SUPPORT_VISIBLE_FOR_TESTING = ClassName.get("android.support.annotation", "VisibleForTesting"); private static final ClassName ANDROIDX_VISIBLE_FOR_TESTING = ClassName.get("androidx.annotation", "VisibleForTesting"); private final ProcessingEnvironment processingEnv; private final TypeElement appGlideModuleType; private final TypeElement libraryGlideModuleType; private int round; ProcessorUtil(ProcessingEnvironment processingEnv) { this.processingEnv = processingEnv; appGlideModuleType = processingEnv.getElementUtils().getTypeElement(APP_GLIDE_MODULE_QUALIFIED_NAME); libraryGlideModuleType = processingEnv.getElementUtils().getTypeElement(LIBRARY_GLIDE_MODULE_QUALIFIED_NAME); } void process() { round++; } boolean isAppGlideModule(TypeElement element) { return processingEnv.getTypeUtils().isAssignable(element.asType(), appGlideModuleType.asType()); } boolean isLibraryGlideModule(TypeElement element) { return processingEnv .getTypeUtils() .isAssignable(element.asType(), libraryGlideModuleType.asType()); } boolean isExtension(TypeElement element) { return element.getAnnotation(GlideExtension.class) != null; } int getOverrideType(ExecutableElement element) { GlideOption glideOption = element.getAnnotation(GlideOption.class); return glideOption.override(); } void writeIndexer(TypeSpec indexer) { writeClass(COMPILER_PACKAGE_NAME, indexer); } void writeClass(String packageName, TypeSpec clazz) { try { debugLog("Writing class:\n" + clazz); JavaFile.builder(packageName, clazz) .skipJavaLangImports(true) .build() .writeTo(processingEnv.getFiler()); } catch (Throwable e) { throw new RuntimeException(e); } } List findAnnotatedElementsInClasses( Set classNames, Class annotationClass) { List result = new ArrayList<>(); for (String glideExtensionClassName : classNames) { TypeElement glideExtension = processingEnv.getElementUtils().getTypeElement(glideExtensionClassName); for (Element element : glideExtension.getEnclosedElements()) { if (element.getAnnotation(annotationClass) != null) { result.add((ExecutableElement) element); } } } return result; } List getElementsFor(Class clazz, RoundEnvironment env) { Collection annotatedElements = env.getElementsAnnotatedWith(clazz); return ElementFilter.typesIn(annotatedElements); } /** * Generates a Javadoc code block for generated methods that delegate to methods in {@link * GlideExtension}s. * *

The generated block looks something like this: * *

   * 
   *   {@literal @see} com.extension.package.name.ExtensionClassName#extensionMethod(arg1, argN)
   * 
   * 
* * @param method The method from the {@link GlideExtension} annotated class that the generated * method this Javadoc will be attached to delegates to. */ CodeBlock generateSeeMethodJavadoc(ExecutableElement method) { // Use the simple name of the containing type instead of just the containing type's TypeMirror // so that we avoid appending or other type arguments to the class and breaking // Javadoc's linking. // With this we get @see RequestOptions#methodName(). // With just ClassName.get(element.getEnclosingElement().asType()), we get: // @see RequestOptions#methodName(). return generateSeeMethodJavadoc( getJavadocSafeName(method.getEnclosingElement()), method.getSimpleName().toString(), method.getParameters()); } /** * Generates a Javadoc block for generated methods that delegate to other methods. * *

The generated block looks something like this: * *

   * 
   *     {@literal @see} com.package.ClassContainingMethod.methodSimpleName(
   *         methodParam1, methodParamN)
   * 
   * 
* * @param nameOfClassContainingMethod The simple class name of the class containing the method * without any generic types like {@literal }. * @param methodSimpleName The name of the method. * @param methodParameters A maybe empty list of all the parameters for the method in question. */ CodeBlock generateSeeMethodJavadoc( TypeName nameOfClassContainingMethod, String methodSimpleName, List methodParameters) { return generateSeeMethodJavadocInternal( nameOfClassContainingMethod, methodSimpleName, Lists.transform( methodParameters, new Function() { @Override public Object apply(VariableElement input) { return getJavadocSafeName(input); } })); } CodeBlock generateSeeMethodJavadoc(TypeName nameOfClassContainingMethod, MethodSpec methodSpec) { return generateSeeMethodJavadocInternal( nameOfClassContainingMethod, methodSpec.name, Lists.transform( methodSpec.parameters, new Function() { @Override public Object apply(ParameterSpec input) { return input.type; } })); } private CodeBlock generateSeeMethodJavadocInternal( TypeName nameOfClassContainingMethod, String methodName, List safeParameterNames) { StringBuilder javadocString = new StringBuilder("@see $T#$L("); List javadocArgs = new ArrayList<>(); javadocArgs.add(nameOfClassContainingMethod); javadocArgs.add(methodName); for (Object param : safeParameterNames) { javadocString.append("$T, "); javadocArgs.add(param); } if (javadocArgs.size() > 2) { javadocString = new StringBuilder(javadocString.substring(0, javadocString.length() - 2)); } javadocString.append(")\n"); return CodeBlock.of(javadocString.toString(), javadocArgs.toArray(new Object[0])); } /** * Returns a safe String to use in a Javadoc that will function in a link. * *

This method exists because by Javadoc doesn't handle type parameters({@literal } in * {@literal RequestOptions} for example). */ private TypeName getJavadocSafeName(Element element) { Types typeUtils = processingEnv.getTypeUtils(); TypeMirror type = element.asType(); if (typeUtils.asElement(type) == null) { // If there is no Element, it's a primitive and can't have additional types, so we're done. return ClassName.get(element.asType()); } Name simpleName = typeUtils.asElement(type).getSimpleName(); return ClassName.bestGuess(simpleName.toString()); } void debugLog(String toLog) { if (DEBUG) { infoLog(toLog); } } void infoLog(String toLog) { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "[" + round + "] " + toLog); } static CodeBlock generateCastingSuperCall(TypeName toReturn, MethodSpec method) { return CodeBlock.builder() .add("return ($T) super.$N(", toReturn, method.name) .add( FluentIterable.from(method.parameters) .transform( new Function() { @Override public String apply(ParameterSpec input) { return input.name; } }) .join(Joiner.on(","))) .add(");\n") .build(); } MethodSpec.Builder overriding(ExecutableElement method) { String methodName = method.getSimpleName().toString(); MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName).addAnnotation(Override.class); Set modifiers = method.getModifiers(); modifiers = new LinkedHashSet<>(modifiers); modifiers.remove(Modifier.ABSTRACT); Modifier defaultModifier = null; // Modifier.DEFAULT doesn't exist until Java 8. try { defaultModifier = Modifier.valueOf("DEFAULT"); } catch (IllegalArgumentException e) { // Ignored. } modifiers.remove(defaultModifier); builder = builder.addModifiers(modifiers); for (TypeParameterElement typeParameterElement : method.getTypeParameters()) { TypeVariable var = (TypeVariable) typeParameterElement.asType(); builder = builder.addTypeVariable(TypeVariableName.get(var)); } builder = builder .returns(TypeName.get(method.getReturnType())) .addParameters(getParameters(method)) .varargs(method.isVarArgs()); for (TypeMirror thrownType : method.getThrownTypes()) { builder = builder.addException(TypeName.get(thrownType)); } return builder; } List getParameters(ExecutableElement method) { return getParameters(method.getParameters()); } List getParameters(List parameters) { List result = new ArrayList<>(); for (VariableElement parameter : parameters) { result.add(getParameter(parameter)); } return dedupedParameters(result); } private static List dedupedParameters(List parameters) { boolean hasDupes = false; Set names = new HashSet<>(); for (ParameterSpec parameter : parameters) { String name = parameter.name; if (names.contains(name)) { hasDupes = true; } else { names.add(name); } } if (hasDupes) { List copy = parameters; parameters = new ArrayList<>(); for (int i = 0; i < copy.size(); i++) { ParameterSpec parameter = copy.get(i); parameters.add( ParameterSpec.builder(parameter.type, parameter.name + i) .addModifiers(parameter.modifiers) .addAnnotations(parameter.annotations) .build()); } } return parameters; } private ParameterSpec getParameter(VariableElement parameter) { TypeName type = TypeName.get(parameter.asType()); return ParameterSpec.builder(type, computeParameterName(parameter, type)) .addModifiers(parameter.getModifiers()) .addAnnotations(getAnnotations(parameter)) .build(); } private static String computeParameterName(VariableElement parameter, TypeName type) { String rawClassName = type.withoutAnnotations().toString(); String name; if (type.isPrimitive() || type.isBoxedPrimitive()) { name = getSmartPrimitiveParameterName(parameter); } else { if (rawClassName.contains("<") && rawClassName.contains(">")) { String[] preGenericSplit = rawClassName.split("<"); String preGeneric = preGenericSplit[0]; String[] postGenericSplit = rawClassName.split(">"); String postGeneric = postGenericSplit[postGenericSplit.length - 1]; if (postGenericSplit.length > 1) { rawClassName = preGeneric + postGeneric; } else { rawClassName = preGeneric; } } String[] qualifiers = rawClassName.split("\\."); rawClassName = qualifiers[qualifiers.length - 1]; rawClassName = applySmartParameterNameReplacements(rawClassName); boolean allCaps = true; for (char c : rawClassName.toCharArray()) { if (Character.isLowerCase(c)) { allCaps = false; break; } } if (allCaps) { name = rawClassName.toLowerCase(Locale.ROOT); } else { int indexOfLastWordStart = 0; char[] chars = rawClassName.toCharArray(); for (int i = 0, charArrayLength = chars.length; i < charArrayLength; i++) { char c = chars[i]; if (Character.isUpperCase(c)) { indexOfLastWordStart = i; } } rawClassName = rawClassName.substring(indexOfLastWordStart, rawClassName.length()); name = Character.toLowerCase(rawClassName.charAt(0)) + rawClassName.substring(1, rawClassName.length()); } } return name; } private static String getSmartPrimitiveParameterName(VariableElement parameter) { for (AnnotationMirror annotation : parameter.getAnnotationMirrors()) { String annotationName = annotation.getAnnotationType().toString().toUpperCase(Locale.ROOT); if (annotationName.endsWith("RES")) { // Catch annotations like StringRes return "id"; } else if (annotationName.endsWith("RANGE")) { // Catch annotations like IntRange return "value"; } } return parameter.getSimpleName().toString(); } private static String applySmartParameterNameReplacements(String name) { name = name.replace("[]", "s"); name = name.replace(Class.class.getSimpleName(), "clazz"); name = name.replace(Object.class.getSimpleName(), "o"); return name; } private List getAnnotations(VariableElement element) { List result = new ArrayList<>(); for (AnnotationMirror mirror : element.getAnnotationMirrors()) { result.add(maybeConvertSupportLibraryAnnotation(mirror)); } return result; } private AnnotationSpec maybeConvertSupportLibraryAnnotation(AnnotationMirror mirror) { String annotationName = mirror.getAnnotationType().asElement().toString(); boolean preferAndroidX = visibleForTesting().equals(ANDROIDX_VISIBLE_FOR_TESTING); ImmutableBiMap map = ImmutableBiMap.builder() .put(SUPPORT_NONNULL_ANNOTATION, ANDROIDX_NONNULL_ANNOTATION) .put(SUPPORT_CHECK_RESULT_ANNOTATION, ANDROIDX_CHECK_RESULT_ANNOTATION) .put(SUPPORT_VISIBLE_FOR_TESTING, ANDROIDX_VISIBLE_FOR_TESTING) .build(); ClassName remapped = null; if (preferAndroidX && annotationName.startsWith("android.support.annotation")) { remapped = ClassName.get((TypeElement) mirror.getAnnotationType().asElement()); } else if (!preferAndroidX && annotationName.startsWith("androidx.annotation")) { remapped = ClassName.get((TypeElement) mirror.getAnnotationType().asElement()); } if (remapped != null && map.containsKey(remapped)) { return AnnotationSpec.builder(map.get(remapped)).build(); } else { return AnnotationSpec.get(mirror); } } ClassName visibleForTesting() { return findAnnotationClassName(ANDROIDX_VISIBLE_FOR_TESTING, SUPPORT_VISIBLE_FOR_TESTING); } ClassName nonNull() { return findAnnotationClassName(ANDROIDX_NONNULL_ANNOTATION, SUPPORT_NONNULL_ANNOTATION); } ClassName checkResult() { return findAnnotationClassName( ANDROIDX_CHECK_RESULT_ANNOTATION, SUPPORT_CHECK_RESULT_ANNOTATION); } static List nonNulls() { return ImmutableList.of( SUPPORT_NONNULL_ANNOTATION, JETBRAINS_NOTNULL_ANNOTATION, ANDROIDX_NONNULL_ANNOTATION); } private ClassName findAnnotationClassName(ClassName androidxName, ClassName supportName) { Elements elements = processingEnv.getElementUtils(); TypeElement visibleForTestingTypeElement = elements.getTypeElement(androidxName.reflectionName()); if (visibleForTestingTypeElement != null) { return androidxName; } return supportName; } List findInstanceMethodsReturning(TypeElement clazz, TypeMirror returnType) { return FluentIterable.from(clazz.getEnclosedElements()) .filter(new FilterPublicMethods(returnType, MethodType.INSTANCE)) .transform(new ToMethod()) .toList(); } List findInstanceMethodsReturning(TypeElement clazz, TypeElement returnType) { return FluentIterable.from(clazz.getEnclosedElements()) .filter(new FilterPublicMethods(returnType, MethodType.INSTANCE)) .transform(new ToMethod()) .toList(); } List findStaticMethodsReturning(TypeElement clazz, TypeElement returnType) { return FluentIterable.from(clazz.getEnclosedElements()) .filter(new FilterPublicMethods(returnType, MethodType.STATIC)) .transform(new ToMethod()) .toList(); } List findStaticMethods(TypeElement clazz) { return FluentIterable.from(clazz.getEnclosedElements()) .filter(new FilterPublicMethods((TypeMirror) null /*returnType*/, MethodType.STATIC)) .transform(new ToMethod()) .toList(); } ImmutableSet findClassValuesFromAnnotationOnClassAsNames( Element clazz, Class annotationClass) { String annotationClassName = annotationClass.getName(); AnnotationValue excludedModuleAnnotationValue = null; for (AnnotationMirror annotationMirror : clazz.getAnnotationMirrors()) { // Two different AnnotationMirrors the same class might not be equal, so compare Strings // instead. This check is necessary because a given class may have multiple Annotations. if (!annotationClassName.equals(annotationMirror.getAnnotationType().toString())) { continue; } var entries = annotationMirror.getElementValues().entrySet(); if (entries.size() != 1) { throw new IllegalArgumentException("Expected single value, but found: " + entries); } excludedModuleAnnotationValue = entries.iterator().next().getValue(); if (excludedModuleAnnotationValue == null) { throw new IllegalArgumentException( "Failed to find value for: " + annotationClass + " from mirrors: " + clazz.getAnnotationMirrors()); } } if (excludedModuleAnnotationValue == null) { return ImmutableSet.of(); } Object value = excludedModuleAnnotationValue.getValue(); if (value instanceof List) { LinkedHashSet out = new LinkedHashSet<>(); for (Object o : (List) value) { AnnotationValue av = (AnnotationValue) o; out.add(qualifiedNameFromTypeMirror((TypeMirror) av.getValue())); } return ImmutableSet.copyOf(out); } else { return ImmutableSet.of(qualifiedNameFromTypeMirror((TypeMirror) value)); } } static String qualifiedNameFromTypeMirror(TypeMirror type) { if (type.getKind() == TypeKind.ERROR) { throw new IllegalArgumentException("Unresolved class type in annotation: " + type); } if (type.getKind() == TypeKind.DECLARED) { DeclaredType dt = (DeclaredType) type; TypeElement te = (TypeElement) dt.asElement(); return te.getQualifiedName().toString(); } return type.toString(); } private enum MethodType { STATIC, INSTANCE } private final class FilterPublicMethods implements Predicate { @Nullable private final TypeMirror returnType; private final MethodType methodType; FilterPublicMethods(@Nullable TypeMirror returnType, MethodType methodType) { this.returnType = returnType; this.methodType = methodType; } FilterPublicMethods(@Nullable TypeElement returnType, MethodType methodType) { this(returnType != null ? returnType.asType() : null, methodType); } @Override public boolean apply(@Nullable Element input) { if (input == null || input.getKind() != ElementKind.METHOD || !input.getModifiers().contains(Modifier.PUBLIC)) { return false; } boolean isStatic = input.getModifiers().contains(Modifier.STATIC); if (methodType == MethodType.STATIC && !isStatic) { return false; } else if (methodType == MethodType.INSTANCE && isStatic) { return false; } ExecutableElement method = (ExecutableElement) input; return returnType == null || isReturnValueTypeMatching(method, returnType); } } boolean isReturnValueTypeMatching(ExecutableElement method, TypeElement expectedReturnType) { return isReturnValueTypeMatching(method, expectedReturnType.asType()); } private boolean isReturnValueTypeMatching( ExecutableElement method, TypeMirror expectedReturnType) { return processingEnv.getTypeUtils().isAssignable(method.getReturnType(), expectedReturnType); } private static final class ToMethod implements Function { @Nullable @Override public ExecutableElement apply(@Nullable Element input) { return (ExecutableElement) input; } } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/RequestBuilderGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.WildcardTypeName; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; /** * Generates a {@code com.bumptech.glide.RequestBuilder} subclass containing all methods from the * base class, all methods from {@code com.bumptech.glide.request.RequestOptions} and all * non-override {@link GlideOption} annotated methods in {@link GlideExtension} annotated classes. * *

Generated code looks like this: * *

 * 
 * public final class GlideRequest extends RequestBuilder {
 *   GlideRequest(Class transcodeClass, RequestBuilder other) {
 *     super(transcodeClass, other);
 *   }
 *
 *   GlideRequest(GlideContext context, RequestManager requestManager,
 *       Class transcodeClass) {
 *     super(context, requestManager ,transcodeClass);
 *   }
 *
 *   {@literal @Override}
 *   protected GlideRequest getDownloadOnlyRequest() {
 *    return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS);
 *   }
 *
 *   /**
 *    * {@literal @see} GlideOptions#dontAnimate()
 *    *\/
 *   public GlideRequest dontAnimate() {
 *     if (getMutableOptions() instanceof GlideOptions) {
 *       this.requestOptions = ((GlideOptions) getMutableOptions()).dontAnimate();
 *     } else {
 *       this.requestOptions = new GlideOptions().apply(this.requestOptions).dontAnimate();
 *     }
 *     return this;
 *   }
 *
 *   /**
 *    * {@literal @see} RequestOptions#sizeMultiplier(float)
 *    *\/
 *   public GlideRequest sizeMultiplier(float sizeMultiplier) {
 *     this.requestOptions = getMutableOptions().sizeMultiplier(sizeMultiplier);
 *     return this;
 *   }
 *
 *   ...
 * }
 * 
 * 
*/ final class RequestBuilderGenerator { private static final String REQUEST_OPTIONS_PACKAGE_NAME = "com.bumptech.glide.request"; private static final String REQUEST_OPTIONS_SIMPLE_NAME = "RequestOptions"; private static final String REQUEST_OPTIONS_QUALIFIED_NAME = REQUEST_OPTIONS_PACKAGE_NAME + "." + REQUEST_OPTIONS_SIMPLE_NAME; private static final String REQUEST_BUILDER_PACKAGE_NAME = "com.bumptech.glide"; private static final String REQUEST_BUILDER_SIMPLE_NAME = "RequestBuilder"; static final String REQUEST_BUILDER_QUALIFIED_NAME = REQUEST_BUILDER_PACKAGE_NAME + "." + REQUEST_BUILDER_SIMPLE_NAME; // Uses package private methods and variables. private static final String GENERATED_REQUEST_BUILDER_SIMPLE_NAME = "GlideRequest"; /** * An arbitrary name of the Generic type in the generated RequestBuilder. e.g. * RequestBuilder */ private static final String TRANSCODE_TYPE_NAME = "TranscodeType"; /** A set of method names to avoid overriding from RequestOptions. */ private static final ImmutableSet EXCLUDED_METHODS_FROM_BASE_REQUEST_OPTIONS = ImmutableSet.of("clone", "apply"); private final ProcessingEnvironment processingEnv; private final ProcessorUtil processorUtil; private final TypeVariableName transcodeTypeName; private final TypeElement requestOptionsType; private final TypeElement requestBuilderType; private ClassName generatedRequestBuilderClassName; private ClassName requestOptionsClassName; private ParameterizedTypeName generatedRequestBuilderOfTranscodeType; RequestBuilderGenerator(ProcessingEnvironment processingEnv, ProcessorUtil processorUtil) { this.processingEnv = processingEnv; this.processorUtil = processorUtil; requestBuilderType = processingEnv.getElementUtils().getTypeElement(REQUEST_BUILDER_QUALIFIED_NAME); transcodeTypeName = TypeVariableName.get(TRANSCODE_TYPE_NAME); requestOptionsType = processingEnv.getElementUtils().getTypeElement(REQUEST_OPTIONS_QUALIFIED_NAME); } TypeSpec generate( String generatedCodePackageName, Set glideExtensionClassNames, @Nullable TypeSpec generatedOptions) { if (generatedOptions != null) { requestOptionsClassName = ClassName.get(generatedCodePackageName, generatedOptions.name); } else { requestOptionsClassName = ClassName.get( RequestOptionsGenerator.REQUEST_OPTIONS_PACKAGE_NAME, RequestOptionsGenerator.BASE_REQUEST_OPTIONS_SIMPLE_NAME); } generatedRequestBuilderClassName = ClassName.get(generatedCodePackageName, GENERATED_REQUEST_BUILDER_SIMPLE_NAME); generatedRequestBuilderOfTranscodeType = ParameterizedTypeName.get(generatedRequestBuilderClassName, transcodeTypeName); RequestOptionsExtensionGenerator requestOptionsExtensionGenerator = new RequestOptionsExtensionGenerator(generatedRequestBuilderOfTranscodeType, processorUtil); ParameterizedTypeName requestBuilderOfTranscodeType = ParameterizedTypeName.get( ClassName.get(REQUEST_BUILDER_PACKAGE_NAME, REQUEST_BUILDER_SIMPLE_NAME), transcodeTypeName); List requestOptionsExtensionMethods = requestOptionsExtensionGenerator.generateInstanceMethodsForExtensions( glideExtensionClassNames); return TypeSpec.classBuilder(GENERATED_REQUEST_BUILDER_SIMPLE_NAME) .addJavadoc( "Contains all public methods from {@link $T}, all options from\n", requestBuilderType) .addJavadoc("{@link $T} and all generated options from\n", requestOptionsType) .addJavadoc("{@link $T} in annotated methods in\n", GlideOption.class) .addJavadoc("{@link $T} annotated classes.\n", GlideExtension.class) .addJavadoc("\n") .addJavadoc("

Generated code, do not modify.\n") .addJavadoc("\n") .addJavadoc("@see $T\n", requestBuilderType) .addJavadoc("@see $T\n", requestOptionsType) .addAnnotation( AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "unused") .addMember("value", "$S", "deprecation") .build()) .addModifiers(Modifier.PUBLIC) .addTypeVariable(transcodeTypeName) .superclass(requestBuilderOfTranscodeType) .addSuperinterface(Cloneable.class) .addMethods(generateConstructors()) .addMethod(generateDownloadOnlyRequestMethod()) .addMethods( generateGeneratedRequestOptionsEquivalents( requestOptionsExtensionMethods, generatedOptions)) .addMethods(generateRequestBuilderOverrides()) .addMethods(requestOptionsExtensionMethods) .build(); } /** * Generates methods with equivalent names and arguments to methods annotated with {@link * GlideOption} in {@link com.bumptech.glide.annotation.GlideExtension}s that return our generated * {@code com.bumptech.glide.RequestBuilder} subclass. */ private List generateGeneratedRequestOptionsEquivalents( final List requestOptionsExtensionMethods, @Nullable final TypeSpec generatedOptions) { if (generatedOptions == null) { return Collections.emptyList(); } return FluentIterable.from(generatedOptions.methodSpecs) .filter( new Predicate() { @Override public boolean apply(MethodSpec input) { return isUsefulGeneratedRequestOption(requestOptionsExtensionMethods, input); } }) .transform( new Function() { @Override public MethodSpec apply(MethodSpec input) { return generateGeneratedRequestOptionEquivalent(input); } }) .toList(); } /** * Returns {@code true} if the given {@link MethodSpec} is a useful method to have in our {@code * com.bumptech.glide.RequestBuilder} subclass. * *

Only newly generated methods will be included in the generated {@code * com.bumptech.glide.request.BaseRequestBuilder} subclass, so we only have to filter out methods * that override other methods to avoid duplicates. */ private boolean isUsefulGeneratedRequestOption( List requestOptionsExtensionMethods, final MethodSpec requestOptionsMethod) { return !EXCLUDED_METHODS_FROM_BASE_REQUEST_OPTIONS.contains(requestOptionsMethod.name) && requestOptionsMethod.hasModifier(Modifier.PUBLIC) && !requestOptionsMethod.hasModifier(Modifier.STATIC) && requestOptionsMethod.returnType.toString().equals(requestOptionsClassName.toString()) && !isExtensionMethod(requestOptionsExtensionMethods, requestOptionsMethod); } private boolean isExtensionMethod( List requestOptionsExtensionMethods, final MethodSpec requestOptionsMethod) { return FluentIterable.from(requestOptionsExtensionMethods) .anyMatch( new Predicate() { @Override public boolean apply(MethodSpec input) { return input.name.equals(requestOptionsMethod.name) && input.parameters.equals(requestOptionsMethod.parameters); } }); } /** * Generates a particular method with an equivalent name and arguments to the given method from * the generated {@code com.bumptech.glide.request.BaseRequestBuilder} subclass. */ private MethodSpec generateGeneratedRequestOptionEquivalent(MethodSpec requestOptionMethod) { CodeBlock callRequestOptionsMethod = CodeBlock.builder() .add(".$N(", requestOptionMethod.name) .add( FluentIterable.from(requestOptionMethod.parameters) .transform( new Function() { @Override public String apply(ParameterSpec input) { return input.name; } }) .join(Joiner.on(", "))) .add(");\n") .build(); MethodSpec.Builder result = MethodSpec.methodBuilder(requestOptionMethod.name) .addJavadoc( processorUtil.generateSeeMethodJavadoc( requestOptionsClassName, requestOptionMethod)) .addModifiers(Modifier.PUBLIC) .varargs(requestOptionMethod.varargs) .addAnnotations( FluentIterable.from(requestOptionMethod.annotations) .filter( new Predicate() { @Override public boolean apply(AnnotationSpec input) { return !input.type.equals(TypeName.get(Override.class)) // SafeVarargs can only be applied to final methods. GlideRequest is // non-final to allow for mocking. && !input.type.equals(TypeName.get(SafeVarargs.class)) // We need to combine warnings below. && !input.type.equals(TypeName.get(SuppressWarnings.class)); } }) .toList()) .addTypeVariables(requestOptionMethod.typeVariables) .addParameters(requestOptionMethod.parameters) .returns(generatedRequestBuilderOfTranscodeType) .addCode("return ($T) super", generatedRequestBuilderOfTranscodeType) .addCode(callRequestOptionsMethod); AnnotationSpec suppressWarnings = buildSuppressWarnings(requestOptionMethod); if (suppressWarnings != null) { result.addAnnotation(suppressWarnings); } return result.build(); } @Nullable private AnnotationSpec buildSuppressWarnings(MethodSpec requestOptionMethod) { Set suppressions = new HashSet<>(); if (requestOptionMethod.annotations.contains( AnnotationSpec.builder(SuppressWarnings.class).build())) { for (AnnotationSpec annotation : requestOptionMethod.annotations) { if (annotation.type.equals(TypeName.get(SuppressWarnings.class))) { List codeBlocks = annotation.members.get("value"); suppressions.addAll( FluentIterable.from(codeBlocks) .transform( new Function() { @Override public String apply(CodeBlock input) { return input.toString(); } }) .toSet()); } } } if (requestOptionMethod.annotations.contains( AnnotationSpec.builder(SafeVarargs.class).build())) { suppressions.add("unchecked"); suppressions.add("varargs"); } if (suppressions.isEmpty()) { return null; } // Enforce ordering across compilers (Internal and External compilers end up disagreeing on the // order produced by the Set additions above.) ArrayList suppressionsList = new ArrayList<>(suppressions); Collections.sort(suppressionsList); AnnotationSpec.Builder builder = AnnotationSpec.builder(SuppressWarnings.class); for (String suppression : suppressionsList) { builder.addMember("value", "$S", suppression); } return builder.build(); } /** * Generates overrides of all methods in {@code com.bumptech.glide.RequestBuilder} that return * {@code com.bumptech.glide.RequestBuilder} so that they return our generated subclass instead. */ private List generateRequestBuilderOverrides() { TypeMirror rawRequestBuilderType = processingEnv.getTypeUtils().erasure(requestBuilderType.asType()); return Lists.transform( processorUtil.findInstanceMethodsReturning(requestBuilderType, rawRequestBuilderType), new Function() { @Override public MethodSpec apply(ExecutableElement input) { return generateRequestBuilderOverride(input); } }); } /** * Generates an override of a particular method in {@code com.bumptech.glide.RequestBuilder} that * returns {@code com.bumptech.glide.RequestBuilder} so that it returns our generated subclass * instead. */ private MethodSpec generateRequestBuilderOverride(ExecutableElement methodToOverride) { // We've already verified that this method returns a RequestBuilder and RequestBuilders have // exactly one type argument, so this is safe unless those assumptions change. TypeMirror typeArgument = ((DeclaredType) methodToOverride.getReturnType()).getTypeArguments().get(0); ParameterizedTypeName generatedRequestBuilderOfType = ParameterizedTypeName.get(generatedRequestBuilderClassName, ClassName.get(typeArgument)); MethodSpec.Builder builder = processorUtil.overriding(methodToOverride).returns(generatedRequestBuilderOfType); builder.addCode( CodeBlock.builder() .add( "return ($T) super.$N(", generatedRequestBuilderOfType, methodToOverride.getSimpleName()) .add( FluentIterable.from(builder.build().parameters) .transform( new Function() { @Override public String apply(ParameterSpec input) { return input.name; } }) .join(Joiner.on(", "))) .add(");\n") .build()); for (AnnotationMirror mirror : methodToOverride.getAnnotationMirrors()) { builder = builder.addAnnotation(AnnotationSpec.get(mirror)); } if (methodToOverride.isVarArgs()) { builder = builder .addModifiers(Modifier.FINAL) .addAnnotation(SafeVarargs.class) .addAnnotation( AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "varargs") .build()); } return builder.build(); } private List generateConstructors() { ParameterizedTypeName classOfTranscodeType = ParameterizedTypeName.get(ClassName.get(Class.class), transcodeTypeName); TypeName wildcardOfObject = WildcardTypeName.subtypeOf(Object.class); ParameterizedTypeName requestBuilderOfWildcardOfObject = ParameterizedTypeName.get(ClassName.get(requestBuilderType), wildcardOfObject); MethodSpec firstConstructor = MethodSpec.constructorBuilder() .addParameter( ParameterSpec.builder(classOfTranscodeType, "transcodeClass") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(requestBuilderOfWildcardOfObject, "other") .addAnnotation(processorUtil.nonNull()) .build()) .addStatement("super($N, $N)", "transcodeClass", "other") .build(); ClassName context = ClassName.get("android.content", "Context"); ClassName glide = ClassName.get("com.bumptech.glide", "Glide"); ClassName requestManager = ClassName.get("com.bumptech.glide", "RequestManager"); MethodSpec secondConstructor = MethodSpec.constructorBuilder() .addParameter( ParameterSpec.builder(glide, "glide") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(requestManager, "requestManager") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(classOfTranscodeType, "transcodeClass") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(context, "context") .addAnnotation(processorUtil.nonNull()) .build()) .addStatement( "super($N, $N ,$N, $N)", "glide", "requestManager", "transcodeClass", "context") .build(); return ImmutableList.of(firstConstructor, secondConstructor); } /** * Overrides the protected downloadOnly method in {@code com.bumptech.glide.RequestBuilder} to * return our generated subclass instead. */ private MethodSpec generateDownloadOnlyRequestMethod() { ParameterizedTypeName generatedRequestBuilderOfFile = ParameterizedTypeName.get(generatedRequestBuilderClassName, ClassName.get(File.class)); return MethodSpec.methodBuilder("getDownloadOnlyRequest") .addAnnotation(Override.class) .addAnnotation(processorUtil.checkResult()) .addAnnotation(processorUtil.nonNull()) .returns(generatedRequestBuilderOfFile) .addModifiers(Modifier.PROTECTED) .addStatement( "return new $T<>($T.class, $N).apply($N)", generatedRequestBuilderClassName, File.class, "this", "DOWNLOAD_ONLY_OPTIONS") .build(); } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/RequestManagerFactoryGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeSpec; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; /** * Generates an implementation of {@code * com.bumptech.glide.manager.RequestManagerRetriever.RequestManagerFactory} that returns a * generated {@code com.bumptech.glide.RequestManager} implementation. * *

Generated {@code com.bumptech.glide.manager.RequestManagerRetriever.RequestManagerFactory} * classes look like this: * *

 * 
 * public class GeneratedRequestManagerFactory
 *     implements RequestManagerRetriever.RequestManagerFactory {
 *   {@literal @Override}
 *   public RequestManager build(Glide glide, Lifecycle lifecycle,
 *       RequestManagerTreeNode treeNode) {
 *     return new GeneratedRequestManager(glide, lifecycle, treeNode);
 *   }
 * }
 * 
 * 
*/ final class RequestManagerFactoryGenerator { private static final String GLIDE_QUALIFIED_NAME = "com.bumptech.glide.Glide"; private static final String LIFECYCLE_QUALIFIED_NAME = "com.bumptech.glide.manager.Lifecycle"; private static final String REQUEST_MANAGER_TREE_NODE_QUALIFIED_NAME = "com.bumptech.glide.manager.RequestManagerTreeNode"; private static final String REQUEST_MANAGER_FACTORY_QUALIFIED_NAME = "com.bumptech.glide.manager.RequestManagerRetriever.RequestManagerFactory"; private static final String REQUEST_MANAGER_QUALIFIED_NAME = "com.bumptech.glide.RequestManager"; private static final ClassName CONTEXT_CLASS_NAME = ClassName.get("android.content", "Context"); static final String GENERATED_REQUEST_MANAGER_FACTORY_PACKAGE_NAME = "com.bumptech.glide"; static final String GENERATED_REQUEST_MANAGER_FACTORY_SIMPLE_NAME = "GeneratedRequestManagerFactory"; private final TypeElement glideType; private final TypeElement lifecycleType; private final TypeElement requestManagerTreeNodeType; private final TypeElement requestManagerFactoryInterface; private final ClassName requestManagerClassName; private final ProcessorUtil processorUtil; RequestManagerFactoryGenerator(ProcessingEnvironment processingEnv, ProcessorUtil processorUtil) { this.processorUtil = processorUtil; Elements elementUtils = processingEnv.getElementUtils(); glideType = elementUtils.getTypeElement(GLIDE_QUALIFIED_NAME); lifecycleType = elementUtils.getTypeElement(LIFECYCLE_QUALIFIED_NAME); requestManagerTreeNodeType = elementUtils.getTypeElement(REQUEST_MANAGER_TREE_NODE_QUALIFIED_NAME); requestManagerFactoryInterface = elementUtils.getTypeElement(REQUEST_MANAGER_FACTORY_QUALIFIED_NAME); TypeElement requestManagerType = elementUtils.getTypeElement(REQUEST_MANAGER_QUALIFIED_NAME); requestManagerClassName = ClassName.get(requestManagerType); } TypeSpec generate(String generatedCodePackageName, TypeSpec generatedRequestManagerSpec) { return TypeSpec.classBuilder(GENERATED_REQUEST_MANAGER_FACTORY_SIMPLE_NAME) .addModifiers(Modifier.FINAL) .addSuperinterface(ClassName.get(requestManagerFactoryInterface)) .addJavadoc("Generated code, do not modify\n") .addMethod( MethodSpec.methodBuilder("build") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addAnnotation(processorUtil.nonNull()) .returns(requestManagerClassName) .addParameter( ParameterSpec.builder(ClassName.get(glideType), "glide") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(ClassName.get(lifecycleType), "lifecycle") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(ClassName.get(requestManagerTreeNodeType), "treeNode") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(CONTEXT_CLASS_NAME, "context") .addAnnotation(processorUtil.nonNull()) .build()) .addStatement( "return new $T(glide, lifecycle, treeNode, context)", ClassName.get(generatedCodePackageName, generatedRequestManagerSpec.name)) .build()) .build(); } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/RequestManagerGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideType; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.collect.Lists; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.MethodSpec.Builder; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; /** * Generates an implementation of {@code com.bumptech.glide.RequestManager} that contains generated * methods from {@link GlideExtension}s and {@link GlideType}. * *

Generated {@code com.bumptech.glide.RequestManager} implementations look like this: * *

 * 
 * public final class GeneratedRequestManager extends RequestManager {
 *   GeneratedRequestManager(Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode) {
 *     super(glide, lifecycle, treeNode);
 *   }
 *
 *   public RequestBuilder asGif() {
 *     RequestBuilder requestBuilder = this.as(GifDrawable.class);
 *     GifOptions.asGif(requestBuilder);
 *     return requestBuilder;
 *   }
 * }
 * 
 * 
*/ final class RequestManagerGenerator { private static final String GLIDE_QUALIFIED_NAME = "com.bumptech.glide.Glide"; private static final String REQUEST_MANAGER_QUALIFIED_NAME = "com.bumptech.glide.RequestManager"; private static final String LIFECYCLE_QUALIFIED_NAME = "com.bumptech.glide.manager.Lifecycle"; private static final String REQUEST_MANAGER_TREE_NODE_QUALIFIED_NAME = "com.bumptech.glide.manager.RequestManagerTreeNode"; private static final ClassName CONTEXT_CLASS_NAME = ClassName.get("android.content", "Context"); private static final String GENERATED_REQUEST_MANAGER_SIMPLE_NAME = "GlideRequests"; private ProcessingEnvironment processingEnv; private final ProcessorUtil processorUtil; private final ClassName requestManagerClassName; private final TypeElement lifecycleType; private final TypeElement requestManagerTreeNodeType; private final TypeElement glideType; private final TypeElement requestManagerType; private final TypeElement requestBuilderType; private ClassName generatedRequestBuilderClassName; RequestManagerGenerator(ProcessingEnvironment processingEnv, ProcessorUtil processorUtil) { this.processingEnv = processingEnv; this.processorUtil = processorUtil; Elements elementUtils = processingEnv.getElementUtils(); requestManagerType = elementUtils.getTypeElement(REQUEST_MANAGER_QUALIFIED_NAME); requestManagerClassName = ClassName.get(requestManagerType); lifecycleType = elementUtils.getTypeElement(LIFECYCLE_QUALIFIED_NAME); requestManagerTreeNodeType = elementUtils.getTypeElement(REQUEST_MANAGER_TREE_NODE_QUALIFIED_NAME); requestBuilderType = elementUtils.getTypeElement(RequestBuilderGenerator.REQUEST_BUILDER_QUALIFIED_NAME); glideType = elementUtils.getTypeElement(GLIDE_QUALIFIED_NAME); } TypeSpec generate( String generatedCodePackageName, @Nullable TypeSpec requestOptions, TypeSpec requestBuilder, Set glideExtensions) { generatedRequestBuilderClassName = ClassName.get(generatedCodePackageName, requestBuilder.name); return TypeSpec.classBuilder(GENERATED_REQUEST_MANAGER_SIMPLE_NAME) .superclass(requestManagerClassName) .addJavadoc( "Includes all additions from methods in {@link $T}s\n" + "annotated with {@link $T}\n" + "\n" + "

Generated code, do not modify\n", GlideExtension.class, GlideType.class) .addAnnotation( AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "deprecation") .build()) .addModifiers(Modifier.PUBLIC) .addMethod(generateAsMethod(generatedCodePackageName, requestBuilder)) .addMethod(generateCallSuperConstructor()) .addMethods(generateExtensionRequestManagerMethods(glideExtensions)) .addMethods(generateRequestManagerRequestManagerMethodOverrides(generatedCodePackageName)) .addMethods(generateRequestManagerRequestBuilderMethodOverrides()) .addMethods( FluentIterable.from( Collections.singletonList( generateOverrideSetRequestOptions( generatedCodePackageName, requestOptions))) .filter(Predicates.notNull())) .build(); } private MethodSpec generateCallSuperConstructor() { return MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter( ParameterSpec.builder(ClassName.get(glideType), "glide") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(ClassName.get(lifecycleType), "lifecycle") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(ClassName.get(requestManagerTreeNodeType), "treeNode") .addAnnotation(processorUtil.nonNull()) .build()) .addParameter( ParameterSpec.builder(CONTEXT_CLASS_NAME, "context") .addAnnotation(processorUtil.nonNull()) .build()) .addStatement("super(glide, lifecycle, treeNode, context)") .build(); } private MethodSpec generateAsMethod(String generatedCodePackageName, TypeSpec requestBuilder) { TypeVariableName resourceType = TypeVariableName.get("ResourceType"); ParameterizedTypeName classOfResouceType = ParameterizedTypeName.get(ClassName.get(Class.class), resourceType); ClassName generatedRequestBuilderClassName = ClassName.get(generatedCodePackageName, requestBuilder.name); ParameterizedTypeName requestBuilderOfResourceType = ParameterizedTypeName.get(generatedRequestBuilderClassName, resourceType); return MethodSpec.methodBuilder("as") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addAnnotation(processorUtil.checkResult()) .addAnnotation(processorUtil.nonNull()) .addTypeVariable(TypeVariableName.get("ResourceType")) .returns(requestBuilderOfResourceType) .addParameter( classOfResouceType.annotated(AnnotationSpec.builder(processorUtil.nonNull()).build()), "resourceClass") .addStatement( "return new $T<>(glide, this, resourceClass, context)", this.generatedRequestBuilderClassName) .build(); } /** Generates the list of overrides of methods that return {@code RequestManager}. */ private List generateRequestManagerRequestManagerMethodOverrides( final String generatedPackageName) { return FluentIterable.from( processorUtil.findInstanceMethodsReturning(requestManagerType, requestManagerType)) .transform( new Function() { @Override public MethodSpec apply(@Nullable ExecutableElement input) { return generateRequestManagerRequestManagerMethodOverride( generatedPackageName, input); } }) .toList(); } private MethodSpec generateRequestManagerRequestManagerMethodOverride( String generatedPackageName, ExecutableElement method) { ClassName generatedRequestManagerName = ClassName.get(generatedPackageName, GENERATED_REQUEST_MANAGER_SIMPLE_NAME); Builder returns = processorUtil .overriding(method) .addAnnotation(processorUtil.nonNull()) .returns(generatedRequestManagerName); return returns .addCode( ProcessorUtil.generateCastingSuperCall(generatedRequestManagerName, returns.build())) .build(); } /** Generates the list of overrides of methods that return {@code RequestBuilder}. */ private List generateRequestManagerRequestBuilderMethodOverrides() { // Without the erasure, this is a RequestBuilder. A RequestBuilder is not assignable to a // RequestBuilder. After type erasure this is a RequestBuilder. A RequestBuilder is // assignable to the raw RequestBuilder. TypeMirror rawRequestBuilder = processingEnv.getTypeUtils().erasure(requestBuilderType.asType()); return FluentIterable.from( processorUtil.findInstanceMethodsReturning(requestManagerType, rawRequestBuilder)) .filter( new Predicate() { @Override public boolean apply(ExecutableElement input) { // Skip the as(Class) method. return !input.getSimpleName().toString().equals("as"); } }) .transform( new Function() { @Override public MethodSpec apply(ExecutableElement input) { return generateRequestManagerRequestBuilderMethodOverride(input); } }) .toList(); } /** * Generates overrides of existing RequestManager methods so that they return our generated * RequestBuilder subtype. */ private MethodSpec generateRequestManagerRequestBuilderMethodOverride( ExecutableElement methodToOverride) { // We've already verified that this method returns a RequestBuilder and RequestBuilders have // exactly one type argument, so this is safe unless those assumptions change. TypeMirror typeArgument = ((DeclaredType) methodToOverride.getReturnType()).getTypeArguments().get(0); ParameterizedTypeName generatedRequestBuilderOfType = ParameterizedTypeName.get(generatedRequestBuilderClassName, ClassName.get(typeArgument)); MethodSpec.Builder builder = processorUtil.overriding(methodToOverride).returns(generatedRequestBuilderOfType); builder.addCode( ProcessorUtil.generateCastingSuperCall(generatedRequestBuilderOfType, builder.build())); for (AnnotationMirror mirror : methodToOverride.getAnnotationMirrors()) { builder.addAnnotation(AnnotationSpec.get(mirror)); } return builder.build(); } private List generateExtensionRequestManagerMethods(Set glideExtensions) { List requestManagerExtensionMethods = processorUtil.findAnnotatedElementsInClasses(glideExtensions, GlideType.class); return Lists.transform( requestManagerExtensionMethods, new Function() { @Override public MethodSpec apply(ExecutableElement input) { return generateAdditionalRequestManagerMethod(input); } }); } // Generates methods added to RequestManager via GlideExtensions. private MethodSpec generateAdditionalRequestManagerMethod(ExecutableElement extensionMethod) { if (extensionMethod.getReturnType().getKind() == TypeKind.VOID) { return generateAdditionalRequestManagerMethodLegacy(extensionMethod); } else { return generateAdditionalRequestManagerMethodNew(extensionMethod); } } private MethodSpec generateAdditionalRequestManagerMethodLegacy( ExecutableElement extensionMethod) { String returnType = processorUtil .findClassValuesFromAnnotationOnClassAsNames(extensionMethod, GlideType.class) .iterator() .next(); ClassName returnTypeClassName = ClassName.bestGuess(returnType); ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(generatedRequestBuilderClassName, returnTypeClassName); return MethodSpec.methodBuilder(extensionMethod.getSimpleName().toString()) .addModifiers(Modifier.PUBLIC) .returns(parameterizedTypeName) .addJavadoc(processorUtil.generateSeeMethodJavadoc(extensionMethod)) .addAnnotation(processorUtil.nonNull()) .addAnnotation(processorUtil.checkResult()) .addStatement( "$T requestBuilder = this.as($T.class)", parameterizedTypeName, returnTypeClassName) .addStatement( "$T.$N(requestBuilder)", extensionMethod.getEnclosingElement(), extensionMethod.getSimpleName()) .addStatement("return requestBuilder") .build(); } private MethodSpec generateAdditionalRequestManagerMethodNew(ExecutableElement extensionMethod) { String returnType = processorUtil .findClassValuesFromAnnotationOnClassAsNames(extensionMethod, GlideType.class) .iterator() .next(); ClassName returnTypeClassName = ClassName.bestGuess(returnType); ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(generatedRequestBuilderClassName, returnTypeClassName); return MethodSpec.methodBuilder(extensionMethod.getSimpleName().toString()) .addModifiers(Modifier.PUBLIC) .returns(parameterizedTypeName) .addJavadoc(processorUtil.generateSeeMethodJavadoc(extensionMethod)) .addAnnotation(processorUtil.nonNull()) .addAnnotation(processorUtil.checkResult()) .addStatement( "return ($T) $T.$N(this.as($T.class))", parameterizedTypeName, extensionMethod.getEnclosingElement(), extensionMethod.getSimpleName(), returnTypeClassName) .build(); } /** * The {@code RequestOptions} subclass should always be our generated subclass type to avoid * inadvertent errors where a different subclass is applied that accidentally wipes out some logic * in overidden methods in our generated subclass. */ @Nullable private MethodSpec generateOverrideSetRequestOptions( String generatedCodePackageName, @Nullable TypeSpec generatedRequestOptions) { if (generatedRequestOptions == null) { return null; } Elements elementUtils = processingEnv.getElementUtils(); TypeElement requestOptionsType = elementUtils.getTypeElement(RequestOptionsGenerator.REQUEST_OPTIONS_QUALIFIED_NAME); // This class may have just been generated and therefore may not be found if we try to obtain // it via Elements, so use just the String version instead. String generatedRequestOptionsQualifiedName = generatedCodePackageName + "." + generatedRequestOptions.name; String methodName = "setRequestOptions"; String parameterName = "toSet"; return MethodSpec.methodBuilder(methodName) .addAnnotation(Override.class) .addModifiers(Modifier.PROTECTED) .addParameter( ParameterSpec.builder(ClassName.get(requestOptionsType), parameterName) .addAnnotation(processorUtil.nonNull()) .build()) .beginControlFlow( "if ($N instanceof $L)", parameterName, generatedRequestOptionsQualifiedName) .addStatement("super.$N($N)", methodName, parameterName) .nextControlFlow("else") .addStatement( "super.setRequestOptions(new $L().apply($N))", generatedRequestOptionsQualifiedName, parameterName) .endControlFlow() .build(); } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/RequestOptionsExtensionGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.GlideOption.OVERRIDE_EXTEND; import com.bumptech.glide.annotation.GlideOption; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeName; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; /** * Generates method overrides for classes that want to mix in {@link GlideOption} annotated methods * in Glide extensions. */ final class RequestOptionsExtensionGenerator { private TypeName containingClassName; private ProcessorUtil processorUtil; RequestOptionsExtensionGenerator(TypeName containingClassName, ProcessorUtil processorUtil) { this.containingClassName = containingClassName; this.processorUtil = processorUtil; } /** * Returns the set of {@link GlideOption} annotated methods in the classes that correspond to the * given extension class names. */ List getRequestOptionExtensionMethods(Set glideExtensionClassNames) { return processorUtil.findAnnotatedElementsInClasses( glideExtensionClassNames, GlideOption.class); } /** * Returns a list containing an override {@link MethodSpec} for all {@link GlideOption} annotated * methods in the classes that correspond to the given extension class names. */ List generateInstanceMethodsForExtensions(Set glideExtensionClassNames) { List requestOptionExtensionMethods = getRequestOptionExtensionMethods(glideExtensionClassNames); List result = new ArrayList<>(requestOptionExtensionMethods.size()); for (ExecutableElement requestOptionsExtensionMethod : requestOptionExtensionMethods) { result.add(generateMethodsForRequestOptionsExtension(requestOptionsExtensionMethod)); } return result; } private MethodSpec generateMethodsForRequestOptionsExtension(ExecutableElement element) { // Assert for legacy versions if (element.getReturnType().getKind() == TypeKind.VOID) { throw new IllegalArgumentException( "The " + element.getSimpleName() + " method annotated with @GlideOption in the " + element.getEnclosingElement().getSimpleName() + " @GlideExtension is using a legacy" + " format that is no longer supported. Please change your method definition so that" + " your @GlideModule annotated methods return BaseRequestOptions objects instead" + " of null."); } int overrideType = processorUtil.getOverrideType(element); String methodName = element.getSimpleName().toString(); MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) .addModifiers(Modifier.PUBLIC) .addJavadoc(processorUtil.generateSeeMethodJavadoc(element)) .varargs(element.isVarArgs()) .returns(containingClassName) .addAnnotation( AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "unchecked") .build()); // The 0th element is expected to be a RequestOptions object. List paramElements = element.getParameters().subList(1, element.getParameters().size()); List parameters = processorUtil.getParameters(paramElements); builder.addParameters(parameters); String extensionRequestOptionsArgument; if (overrideType == OVERRIDE_EXTEND) { builder .addJavadoc( processorUtil.generateSeeMethodJavadoc( containingClassName, methodName, paramElements)) .addAnnotation(Override.class); List methodArgs = new ArrayList<>(); methodArgs.add(element.getSimpleName().toString()); StringBuilder methodLiterals = new StringBuilder(); if (!parameters.isEmpty()) { for (ParameterSpec parameter : parameters) { methodLiterals.append("$L, "); methodArgs.add(parameter.name); } methodLiterals = new StringBuilder(methodLiterals.substring(0, methodLiterals.length() - 2)); } extensionRequestOptionsArgument = CodeBlock.builder() .add("super.$N(" + methodLiterals + ")", methodArgs.toArray(new Object[0])) .build() .toString(); } else { extensionRequestOptionsArgument = "this"; } List args = new ArrayList<>(); StringBuilder code = new StringBuilder("return ($T) $T.$L($L, "); args.add(containingClassName); args.add(ClassName.get(element.getEnclosingElement().asType())); args.add(element.getSimpleName().toString()); args.add(extensionRequestOptionsArgument); if (!parameters.isEmpty()) { for (ParameterSpec parameter : parameters) { code.append("$L, "); args.add(parameter.name); } } code = new StringBuilder(code.substring(0, code.length() - 2)); code.append(")"); builder.addStatement(code.toString(), args.toArray(new Object[0])); builder.addAnnotation(processorUtil.checkResult()).addAnnotation(processorUtil.nonNull()); return builder.build(); } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/RequestOptionsGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.CodeBlock.Builder; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; /** * Generates a new implementation of {@code com.bumptech.glide.request.RequestOptions} containing * static versions of methods included in the base class and static and instance versions of all * methods annotated with {@link GlideOption} in classes annotated with {@link GlideExtension}. * *

The generated class looks something like this: * *

 * 
 * public final class GlideOptions extends com.bumptech.glide.request.RequestOptions {
 *
 *   public static com.google.android.apps.photos.glide.GlideOptions signatureOf(
 *       com.bumptech.glide.load.Key arg0) {
 *     return new com.google.android.apps.photos.glide.GlideOptions()
 *         .apply(com.bumptech.glide.request.RequestOptions.signatureOf(arg0));
 *   }
 *
 *   ... // The rest of the static versions of methods from RequestOptions go here.
 *
 *   // Now on to methods generated from an extension:
 *   public com.bumptech.glide.GlideOptions dontAnimate() {
 *     com.bumptech.glide.integration.gifdecoder.GifOptions.dontAnimate(this);
 *     return this;
 *   }
 *
 *   public static com.bumptech.glide.GlideOptions noAnimation() {
 *     return new com.bumptech.glide.GlideOptions().dontAnimate();
 *   }
 * }
 * 
 * 
*/ final class RequestOptionsGenerator { private static final String GENERATED_REQUEST_OPTIONS_SIMPLE_NAME = "GlideOptions"; static final String REQUEST_OPTIONS_PACKAGE_NAME = "com.bumptech.glide.request"; private static final String REQUEST_OPTIONS_SIMPLE_NAME = "RequestOptions"; static final String REQUEST_OPTIONS_QUALIFIED_NAME = REQUEST_OPTIONS_PACKAGE_NAME + "." + REQUEST_OPTIONS_SIMPLE_NAME; static final String BASE_REQUEST_OPTIONS_SIMPLE_NAME = "BaseRequestOptions"; static final String BASE_REQUEST_OPTIONS_QUALIFIED_NAME = REQUEST_OPTIONS_PACKAGE_NAME + "." + BASE_REQUEST_OPTIONS_SIMPLE_NAME; private int nextFieldId; private final ClassName requestOptionsName; private final TypeElement requestOptionsType; private final ProcessorUtil processorUtil; private final RequestOptionsOverrideGenerator requestOptionsOverrideGenerator; private ClassName glideOptionsName; RequestOptionsGenerator( ProcessingEnvironment processingEnvironment, ProcessorUtil processorUtil) { this.processorUtil = processorUtil; requestOptionsName = ClassName.get(REQUEST_OPTIONS_PACKAGE_NAME, REQUEST_OPTIONS_SIMPLE_NAME); requestOptionsType = processingEnvironment.getElementUtils().getTypeElement(REQUEST_OPTIONS_QUALIFIED_NAME); requestOptionsOverrideGenerator = new RequestOptionsOverrideGenerator(processingEnvironment, processorUtil); } TypeSpec generate(String generatedCodePackageName, Set glideExtensionClassNames) { glideOptionsName = ClassName.get(generatedCodePackageName, GENERATED_REQUEST_OPTIONS_SIMPLE_NAME); RequestOptionsExtensionGenerator requestOptionsExtensionGenerator = new RequestOptionsExtensionGenerator(glideOptionsName, processorUtil); List instanceMethodsForExtensions = FluentIterable.from( requestOptionsExtensionGenerator.generateInstanceMethodsForExtensions( glideExtensionClassNames)) .transform( new Function() { @Override public MethodAndStaticVar apply(MethodSpec input) { return new MethodAndStaticVar(input); } }) .toList(); List staticMethodsForExtensions = FluentIterable.from( requestOptionsExtensionGenerator.getRequestOptionExtensionMethods( glideExtensionClassNames)) .filter( new Predicate() { @Override public boolean apply(ExecutableElement input) { return !skipStaticMethod(input); } }) .transform( new Function() { @Override public MethodAndStaticVar apply(ExecutableElement input) { return generateStaticMethodEquivalentForExtensionMethod(input); } }) .toList(); List methodsForExtensions = new ArrayList<>(); methodsForExtensions.addAll(instanceMethodsForExtensions); methodsForExtensions.addAll(staticMethodsForExtensions); Set extensionMethodSignatures = ImmutableSet.copyOf( Iterables.transform( methodsForExtensions, new Function() { @Override public MethodSignature apply(MethodAndStaticVar f) { return new MethodSignature(f.method); } })); List staticOverrides = generateStaticMethodOverridesForRequestOptions(); List instanceOverrides = requestOptionsOverrideGenerator.generateInstanceMethodOverridesForRequestOptions( glideOptionsName); List allMethodsAndStaticVars = new ArrayList<>(); for (MethodAndStaticVar item : staticOverrides) { if (extensionMethodSignatures.contains(new MethodSignature(item.method))) { continue; } allMethodsAndStaticVars.add(item); } for (MethodSpec methodSpec : instanceOverrides) { if (extensionMethodSignatures.contains(new MethodSignature(methodSpec))) { continue; } allMethodsAndStaticVars.add(new MethodAndStaticVar(methodSpec)); } allMethodsAndStaticVars.addAll(methodsForExtensions); TypeSpec.Builder classBuilder = TypeSpec.classBuilder(GENERATED_REQUEST_OPTIONS_SIMPLE_NAME) .addAnnotation( AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "deprecation") .build()) .addJavadoc(generateClassJavadoc(glideExtensionClassNames)) .addModifiers(Modifier.FINAL) .addModifiers(Modifier.PUBLIC) .addSuperinterface(Cloneable.class) .superclass(requestOptionsName); for (MethodAndStaticVar methodAndStaticVar : allMethodsAndStaticVars) { if (methodAndStaticVar.method != null) { classBuilder.addMethod(methodAndStaticVar.method); } if (methodAndStaticVar.staticField != null) { classBuilder.addField(methodAndStaticVar.staticField); } } return classBuilder.build(); } private CodeBlock generateClassJavadoc(Set glideExtensionClassNames) { Builder builder = CodeBlock.builder() .add( "Automatically generated from {@link $T} annotated classes.\n", GlideExtension.class) .add("\n") .add("@see $T\n", requestOptionsName); for (String glideExtensionClass : glideExtensionClassNames) { builder.add("@see $T\n", ClassName.bestGuess(glideExtensionClass)); } return builder.build(); } private List generateStaticMethodOverridesForRequestOptions() { List staticMethodsThatReturnRequestOptions = processorUtil.findStaticMethodsReturning(requestOptionsType, requestOptionsType); List staticMethods = new ArrayList<>(); for (ExecutableElement element : staticMethodsThatReturnRequestOptions) { if (element.getAnnotation(Deprecated.class) != null) { continue; } staticMethods.add(generateStaticMethodEquivalentForRequestOptionsStaticMethod(element)); } return staticMethods; } /** * This method is a bit of a hack, but it lets us tie the static version of a method with the * instance version. In turn that lets us call the instance versions on the generated subclass, * instead of just delegating to the RequestOptions static methods. Using the instance methods on * the generated subclass allows our static methods to properly call code that overrides an * existing method in RequestOptions. * *

The string names here just map between the static methods in {@code * com.bumptech.glide.request.RequestOptions} and the instance methods they call. */ private static String getInstanceMethodNameFromStaticMethodName(String staticMethodName) { String equivalentInstanceMethodName; if ("bitmapTransform".equals(staticMethodName)) { equivalentInstanceMethodName = "transform"; } else if ("decodeTypeOf".equals(staticMethodName)) { equivalentInstanceMethodName = "decode"; } else if (staticMethodName.endsWith("Transform")) { equivalentInstanceMethodName = staticMethodName.substring(0, staticMethodName.length() - 9); } else if (staticMethodName.endsWith("Of")) { equivalentInstanceMethodName = staticMethodName.substring(0, staticMethodName.length() - 2); } else if ("noTransformation".equals(staticMethodName)) { equivalentInstanceMethodName = "dontTransform"; } else if ("noAnimation".equals(staticMethodName)) { equivalentInstanceMethodName = "dontAnimate"; } else if (staticMethodName.equals("option")) { equivalentInstanceMethodName = "set"; } else { throw new IllegalArgumentException("Unrecognized static method name: " + staticMethodName); } return equivalentInstanceMethodName; } private MethodAndStaticVar generateStaticMethodEquivalentForRequestOptionsStaticMethod( ExecutableElement staticMethod) { boolean memoize = memoizeStaticMethodFromArguments(staticMethod); String staticMethodName = staticMethod.getSimpleName().toString(); String equivalentInstanceMethodName = getInstanceMethodNameFromStaticMethodName(staticMethodName); MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(staticMethodName) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addJavadoc(processorUtil.generateSeeMethodJavadoc(staticMethod)) .returns(glideOptionsName); StringBuilder createNewOptionAndCall = createNewOptionAndCall( memoize, methodSpecBuilder, "new $T().$N(", processorUtil.getParameters(staticMethod)); FieldSpec requiredStaticField = null; if (memoize) { // Generates code that looks like: // if (GlideOptions. == null) { // GlideOptions. = new GlideOptions().().autoClone() // } // Mix in an incrementing unique id to handle method overloading. String staticVariableName = staticMethodName + nextFieldId++; requiredStaticField = FieldSpec.builder(glideOptionsName, staticVariableName) .addModifiers(Modifier.PRIVATE, Modifier.STATIC) .build(); methodSpecBuilder .beginControlFlow("if ($T.$N == null)", glideOptionsName, staticVariableName) .addStatement( "$T.$N =\n" + createNewOptionAndCall + ".$N", glideOptionsName, staticVariableName, glideOptionsName, equivalentInstanceMethodName, "autoClone()") .endControlFlow() .addStatement("return $T.$N", glideOptionsName, staticVariableName); } else { // Generates code that looks like: // return new GlideOptions().() methodSpecBuilder.addStatement( "return " + createNewOptionAndCall, glideOptionsName, equivalentInstanceMethodName); } List typeParameters = staticMethod.getTypeParameters(); for (TypeParameterElement typeParameterElement : typeParameters) { methodSpecBuilder.addTypeVariable( TypeVariableName.get(typeParameterElement.getSimpleName().toString())); } methodSpecBuilder .addAnnotation(processorUtil.checkResult()) .addAnnotation(processorUtil.nonNull()); return new MethodAndStaticVar(methodSpecBuilder.build(), requiredStaticField); } @SuppressWarnings("checkstyle:UnnecessaryParentheses") // Readability private static boolean memoizeStaticMethodFromArguments(ExecutableElement staticMethod) { return staticMethod.getParameters().isEmpty() || (staticMethod.getParameters().size() == 1 && staticMethod .getParameters() .get(0) .getSimpleName() .toString() .equals("android.content.Context")); } private StringBuilder createNewOptionAndCall( boolean memoize, MethodSpec.Builder methodSpecBuilder, String start, List specs) { StringBuilder createNewOptionAndCall = new StringBuilder(start); if (!specs.isEmpty()) { methodSpecBuilder.addParameters(specs); for (ParameterSpec parameter : specs) { createNewOptionAndCall.append(parameter.name); // use the Application Context to avoid memory leaks. if (memoize && isAndroidContext(parameter)) { createNewOptionAndCall.append(".getApplicationContext()"); } createNewOptionAndCall.append(", "); } createNewOptionAndCall = new StringBuilder( createNewOptionAndCall.substring(0, createNewOptionAndCall.length() - 2)); } createNewOptionAndCall.append(")"); return createNewOptionAndCall; } private boolean isAndroidContext(ParameterSpec parameter) { return parameter.type.toString().equals("android.content.Context"); } private MethodAndStaticVar generateStaticMethodEquivalentForExtensionMethod( ExecutableElement instanceMethod) { String staticMethodName = getStaticMethodName(instanceMethod); String instanceMethodName = instanceMethod.getSimpleName().toString(); if (Strings.isNullOrEmpty(staticMethodName)) { if (instanceMethodName.startsWith("dont")) { staticMethodName = "no" + instanceMethodName.replace("dont", ""); } else { staticMethodName = instanceMethodName + "Of"; } } boolean memoize = memoizeStaticMethodFromAnnotation(instanceMethod); //noinspection ResultOfMethodCallIgnored Preconditions.checkNotNull(staticMethodName); MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(staticMethodName) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addJavadoc(processorUtil.generateSeeMethodJavadoc(instanceMethod)) .varargs(instanceMethod.isVarArgs()) .returns(glideOptionsName); List parameters = instanceMethod.getParameters(); // Always remove the first parameter because it's always RequestOptions in extensions. The // actual method we want to generate will pass the RequestOptions in to the extension method, // but should not itself require a RequestOptions object to be passed in. if (parameters.isEmpty()) { throw new IllegalArgumentException("Expected non-empty parameters for: " + instanceMethod); } // Remove is not supported. parameters = parameters.subList(1, parameters.size()); StringBuilder createNewOptionAndCall = createNewOptionAndCall( memoize, methodSpecBuilder, "new $T().$L(", processorUtil.getParameters(parameters)); FieldSpec requiredStaticField = null; if (memoize) { // Generates code that looks like: // if (GlideOptions. == null) { // GlideOptions. = new GlideOptions().().autoClone() // } // Mix in an incrementing unique id to handle method overloading. String staticVariableName = staticMethodName + nextFieldId++; requiredStaticField = FieldSpec.builder(glideOptionsName, staticVariableName) .addModifiers(Modifier.PRIVATE, Modifier.STATIC) .build(); methodSpecBuilder .beginControlFlow("if ($T.$N == null)", glideOptionsName, staticVariableName) .addStatement( "$T.$N =\n" + createNewOptionAndCall + ".$N", glideOptionsName, staticVariableName, glideOptionsName, instanceMethodName, "autoClone()") .endControlFlow() .addStatement("return $T.$N", glideOptionsName, staticVariableName); } else { // Generates code that looks like: // return new GlideOptions().() methodSpecBuilder.addStatement( "return " + createNewOptionAndCall, glideOptionsName, instanceMethodName); } List typeParameters = instanceMethod.getTypeParameters(); for (TypeParameterElement typeParameterElement : typeParameters) { methodSpecBuilder.addTypeVariable( TypeVariableName.get(typeParameterElement.getSimpleName().toString())); } methodSpecBuilder.addAnnotation(processorUtil.checkResult()); return new MethodAndStaticVar(methodSpecBuilder.build(), requiredStaticField); } @Nullable private static String getStaticMethodName(ExecutableElement element) { GlideOption glideOption = element.getAnnotation(GlideOption.class); String result = glideOption != null ? glideOption.staticMethodName() : null; return Strings.emptyToNull(result); } private static boolean memoizeStaticMethodFromAnnotation(ExecutableElement element) { GlideOption glideOption = element.getAnnotation(GlideOption.class); return glideOption != null && glideOption.memoizeStaticMethod(); } private static boolean skipStaticMethod(ExecutableElement element) { GlideOption glideOption = element.getAnnotation(GlideOption.class); return glideOption != null && glideOption.skipStaticMethod(); } private static final class MethodAndStaticVar { @Nullable final MethodSpec method; @Nullable final FieldSpec staticField; MethodAndStaticVar(@Nullable MethodSpec method) { this(method, null /*staticField*/); } MethodAndStaticVar(@Nullable MethodSpec method, @Nullable FieldSpec staticField) { this.method = method; this.staticField = staticField; } } private static final class MethodSignature { private final TypeName returnType; private final List parameterTypes; private final boolean isStatic; private final String name; MethodSignature(MethodSpec spec) { name = spec.name; isStatic = spec.modifiers.contains(Modifier.STATIC); returnType = spec.returnType; parameterTypes = Lists.transform( spec.parameters, new Function() { @Nullable @Override public TypeName apply(ParameterSpec parameterSpec) { return parameterSpec.type; } }); } @Override public boolean equals(Object o) { if (o instanceof MethodSignature) { MethodSignature other = (MethodSignature) o; return name.equals(other.name) && returnType.equals(other.returnType) && parameterTypes.equals(other.parameterTypes) && isStatic == other.isStatic; } return false; } @Override public int hashCode() { return Objects.hashCode(name, returnType, parameterTypes, isStatic); } } } ================================================ FILE: annotation/compiler/src/main/java/com/bumptech/glide/annotation/compiler/RequestOptionsOverrideGenerator.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.RequestOptionsGenerator.BASE_REQUEST_OPTIONS_QUALIFIED_NAME; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeName; import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; /** * Generates overrides for BaseRequestOptions methods so that subclasses' methods return the * subclass type, not just BaseRequestOptions. */ final class RequestOptionsOverrideGenerator { private final TypeElement baseRequestOptionsType; private ProcessorUtil processorUtil; RequestOptionsOverrideGenerator( ProcessingEnvironment processingEnv, ProcessorUtil processorUtil) { this.processorUtil = processorUtil; baseRequestOptionsType = processingEnv.getElementUtils().getTypeElement(BASE_REQUEST_OPTIONS_QUALIFIED_NAME); } List generateInstanceMethodOverridesForRequestOptions(TypeName typeToOverrideIn) { return generateInstanceMethodOverridesForRequestOptions( typeToOverrideIn, Collections.emptySet()); } List generateInstanceMethodOverridesForRequestOptions( final TypeName typeToOverrideIn, final Set excludedMethods) { return FluentIterable.from( processorUtil.findInstanceMethodsReturning( baseRequestOptionsType, baseRequestOptionsType)) .filter( new Predicate() { @Override public boolean apply(ExecutableElement input) { return !excludedMethods.contains(input.getSimpleName().toString()); } }) .transform( new Function() { @Override public MethodSpec apply(ExecutableElement input) { return generateRequestOptionOverride(typeToOverrideIn, input); } }) .toList(); } private MethodSpec generateRequestOptionOverride( TypeName typeToOverrideIn, ExecutableElement methodToOverride) { MethodSpec.Builder result = processorUtil.overriding(methodToOverride).returns(typeToOverrideIn); result.addCode( CodeBlock.builder() .add("return ($T) super.$N(", typeToOverrideIn, methodToOverride.getSimpleName()) .add( FluentIterable.from(result.build().parameters) .transform( new Function() { @Override public String apply(ParameterSpec input) { return input.name; } }) .join(Joiner.on(", "))) .add(");\n") .build()); if (methodToOverride.getSimpleName().toString().contains("transform") && methodToOverride.isVarArgs()) { result .addModifiers(Modifier.FINAL) .addAnnotation(SafeVarargs.class) .addAnnotation( AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "varargs") .build()); } for (AnnotationMirror mirror : methodToOverride.getAnnotationMirrors()) { result.addAnnotation(AnnotationSpec.get(mirror)); } return result.build(); } } ================================================ FILE: annotation/compiler/src/main/resources/META-INF/gradle/incremental.annotation.processors ================================================ com.bumptech.glide.annotation.compiler.GlideAnnotationProcessor,aggregating ================================================ FILE: annotation/compiler/test/.gitignore ================================================ /build ================================================ FILE: annotation/compiler/test/build.gradle ================================================ apply plugin: 'com.android.library' android { sourceSets { test { resources { // *.java is excluded by default... setExcludes([]) } // TODO: Re-enable these tests after fixing import orders. java { exclude "**/AppGlideModuleWithExcludesTest.java" exclude "**/AppGlideModuleWithLibraryInPackageTest.java" exclude "**/AppGlideModuleWithMultipleExcludesTest.java" exclude "**/EmptyAppAndLibraryGlideModulesTest.java" exclude "**/GlideExtensionWithOptionTest.java" exclude "**/GlideExtensionWithTypeTest.java" exclude "**/GlideExtensionWithTypeTest.java" } } } } afterEvaluate { lint.enabled = false compileReleaseJavaWithJavac.enabled = false } android { namespace 'com.bumptech.glide.annotation.compiler.test' compileSdk libs.versions.compile.sdk.version.get() defaultConfig { minSdk libs.versions.min.sdk.version.get() as int targetSdk libs.versions.target.sdk.version.get() as int versionName VERSION_NAME as String } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } testOptions { unitTests { all { Test testTask -> testTask.maxParallelForks = 2 } } } } // This special test only submodule exists because adding the :glide dependency seems to break // the annotation processor dependency chain for the internal sample apps. It's also somewhat // easier to parse as a separate module given the existing complexity here and in the compiler // build.gradle file. dependencies { testImplementation project(':glide') testImplementation project(':annotation:compiler') testImplementation libs.junit testImplementation libs.javapoet testImplementation libs.findbugs.jsr305 // Using 0.10 of compile-testing is required for Android Studio to function, but not for the // gradle build. Not yet clear why, but it looks like some kind of version conflict between // javapoet, guava and/or truth. //noinspection GradleDependency testImplementation ("com.google.testing.compile:compile-testing:0.10") { // We don't use this and including it requires us to list it separatel which would be // confusing. exclude group: "com.google.auto.value", module: "auto-value" } testImplementation libs.androidx.annotation testImplementation libs.androidx.fragment // TODO: Find some way to include a similar dependency on java 9+ and re-enable these tests in gradle. // testImplementation files(Jvm.current().getJre().homeDir.getAbsolutePath()+'/lib/rt.jar') testAnnotationProcessor project(':annotation:compiler') testAnnotationProcessor libs.autoservice } task regenerateTestResources { group 'Verification' description 'Regenerates all test resource files under annotation/compiler/test/src/test/resources that are compared against the current output to detect regressions' tasks.withType(Test) { systemProperties.put("com.bumptech.glide.annotation.compiler.test.regenerate.path", projectDir) } doFirst { println("Regenerating test resources....") } doLast { println("Finished regenerating test resources") } } afterEvaluate { regenerateTestResources.finalizedBy(testReleaseUnitTest) } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/AppGlideModuleWithExcludesTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.appResource; import static com.bumptech.glide.annotation.compiler.test.Util.emptyLibraryModule; import static com.bumptech.glide.annotation.compiler.test.Util.glide; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.ReferencedResource; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests AppGlideModules that use the @Excludes annotation with a single excluded Module class. */ @RunWith(JUnit4.class) public class AppGlideModuleWithExcludesTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile(forResource("AppModuleWithExcludes.java"), emptyLibraryModule()); assertThat(compilation).succeededWithoutWarnings(); } @Override public Compilation getCompilation() { return compilation; } @Test @ReferencedResource public void compilation_generatesExpectedGlideOptionsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideOptions")) .hasSourceEquivalentTo(appResource("GlideOptions.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequest")) .hasSourceEquivalentTo(appResource("GlideRequest.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequests")) .hasSourceEquivalentTo(appResource("GlideRequests.java")); } @Test @ReferencedResource public void compilationGeneratesExpectedGlideAppClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideApp")) .hasSourceEquivalentTo(appResource("GlideApp.java")); } @Test public void compilation_generatesExpectedGeneratedAppGlideModuleImpl() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedAppGlideModuleImpl")) .hasSourceEquivalentTo(forResource("GeneratedAppGlideModuleImpl.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedRequestManagerFactory() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedRequestManagerFactory")) .hasSourceEquivalentTo(appResource("GeneratedRequestManagerFactory.java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/AppGlideModuleWithLibraryInPackageTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.appResource; import static com.bumptech.glide.annotation.compiler.test.Util.glide; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.ReferencedResource; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Tests AppGlideModules that use the @Excludes annotation with a single excluded Module class in a * strangely named subpackage. */ @RunWith(JUnit4.class) public class AppGlideModuleWithLibraryInPackageTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( forResource("AppModuleWithLibraryInPackage.java"), forResource("LibraryModuleInPackage.java")); assertThat(compilation).succeededWithoutWarnings(); } @Override public Compilation getCompilation() { return compilation; } @Test @ReferencedResource public void compilation_generatesExpectedGlideOptionsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideOptions")) .hasSourceEquivalentTo(appResource("GlideOptions.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequest")) .hasSourceEquivalentTo(appResource("GlideRequest.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequests")) .hasSourceEquivalentTo(appResource("GlideRequests.java")); } @Test @ReferencedResource public void compilationGeneratesExpectedGlideAppClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideApp")) .hasSourceEquivalentTo(appResource("GlideApp.java")); } @Test public void compilation_generatesExpectedGeneratedAppGlideModuleImpl() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedAppGlideModuleImpl")) .hasSourceEquivalentTo(forResource("GeneratedAppGlideModuleImpl.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedRequestManagerFactory() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedRequestManagerFactory")) .hasSourceEquivalentTo(appResource("GeneratedRequestManagerFactory.java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/AppGlideModuleWithMultipleExcludesTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.appResource; import static com.bumptech.glide.annotation.compiler.test.Util.glide; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.ReferencedResource; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Tests AppGlideModules that use the @Excludes annotation with multiple excluded Module classes. */ @RunWith(JUnit4.class) public class AppGlideModuleWithMultipleExcludesTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( forResource("AppModuleWithMultipleExcludes.java"), forResource("EmptyLibraryModule1.java"), forResource("EmptyLibraryModule2.java")); assertThat(compilation).succeededWithoutWarnings(); } @Override public Compilation getCompilation() { return compilation; } @Test @ReferencedResource public void compilation_generatesExpectedGlideOptionsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideOptions")) .hasSourceEquivalentTo(appResource("GlideOptions.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequest")) .hasSourceEquivalentTo(appResource("GlideRequest.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequests")) .hasSourceEquivalentTo(appResource("GlideRequests.java")); } @Test @ReferencedResource public void compilationGeneratesExpectedGlideAppClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideApp")) .hasSourceEquivalentTo(appResource("GlideApp.java")); } @Test public void compilation_generatesExpectedGeneratedAppGlideModuleImpl() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedAppGlideModuleImpl")) .hasSourceEquivalentTo(forResource("GeneratedAppGlideModuleImpl.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedRequestManagerFactory() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedRequestManagerFactory")) .hasSourceEquivalentTo(appResource("GeneratedRequestManagerFactory.java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/EmptyAppAndLibraryGlideModulesTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.annotation; import static com.bumptech.glide.annotation.compiler.test.Util.appResource; import static com.bumptech.glide.annotation.compiler.test.Util.emptyAppModule; import static com.bumptech.glide.annotation.compiler.test.Util.emptyLibraryModule; import static com.bumptech.glide.annotation.compiler.test.Util.glide; import static com.bumptech.glide.annotation.compiler.test.Util.libraryResource; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.ReferencedResource; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Tests adding both an empty {@link com.bumptech.glide.module.AppGlideModule} and an empty {@link * com.bumptech.glide.module.LibraryGlideModule} in a single project. */ @RunWith(JUnit4.class) public class EmptyAppAndLibraryGlideModulesTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile(emptyAppModule(), emptyLibraryModule()); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_generatesAllExpectedFiles() { Truth.assertThat(compilation.generatedSourceFiles()).hasSize(7); } @Test @ReferencedResource public void compilation_generatesExpectedGlideOptionsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideOptions")) .hasSourceEquivalentTo(appResource("GlideOptions.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequest")) .hasSourceEquivalentTo(appResource("GlideRequest.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequests")) .hasSourceEquivalentTo(appResource("GlideRequests.java")); } @Test @ReferencedResource public void compilationGeneratesExpectedGlideAppClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideApp")) .hasSourceEquivalentTo(appResource("GlideApp.java")); } @Test public void compilation_generatesExpectedGeneratedAppGlideModuleImpl() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedAppGlideModuleImpl")) .hasSourceEquivalentTo(forResource("GeneratedAppGlideModuleImpl.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedRequestManagerFactory() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedRequestManagerFactory")) .hasSourceEquivalentTo(appResource("GeneratedRequestManagerFactory.java")); } @Test @ReferencedResource public void compilation_generatesExpectedIndexer() throws IOException { String expectedClassName = "GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule"; assertThat(compilation) .generatedSourceFile(annotation(expectedClassName)) .hasSourceEquivalentTo(libraryResource(expectedClassName + ".java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/EmptyAppGlideModuleTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.glide; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests adding a single {@link com.bumptech.glide.test.EmptyAppModule} in a project. */ @RunWith(JUnit4.class) public class EmptyAppGlideModuleTest implements CompilationProvider { private static final String MODULE_NAME = "EmptyAppModule.java"; @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac().withProcessors(new GlideAnnotationProcessor()).compile(forResource(MODULE_NAME)); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_generatesAllExpectedFiles() { Truth.assertThat(compilation.generatedSourceFiles()).hasSize(6); } @Test public void compilation_generatesExpectedGlideOptionsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideOptions")) .hasSourceEquivalentTo(forResource("GlideOptions.java")); } @Test public void compilation_generatesExpectedGlideRequestClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequest")) .hasSourceEquivalentTo(forResource("GlideRequest.java")); } @Test public void compilation_generatesExpectedGlideRequestsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequests")) .hasSourceEquivalentTo(forResource("GlideRequests.java")); } @Test public void compilationGeneratesExpectedGlideAppClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideApp")) .hasSourceEquivalentTo(forResource("GlideApp.java")); } @Test public void compilation_generatesExpectedGeneratedAppGlideModuleImpl() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedAppGlideModuleImpl")) .hasSourceEquivalentTo(forResource("GeneratedAppGlideModuleImpl.java")); } @Test public void compilation_generatesExpectedGeneratedRequestManagerFactory() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedRequestManagerFactory")) .hasSourceEquivalentTo(forResource("GeneratedRequestManagerFactory.java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/EmptyLibraryGlideModuleTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.annotation; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests adding a single {@link com.bumptech.glide.module.LibraryGlideModule} in a project. */ @RunWith(JUnit4.class) public class EmptyLibraryGlideModuleTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private static final String MODULE_NAME = "EmptyLibraryModule.java"; private Compilation compilation; @Before public void setUp() { compilation = javac().withProcessors(new GlideAnnotationProcessor()).compile(forResource(MODULE_NAME)); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_generatesAllExpectedFiles() { Truth.assertThat(compilation.generatedSourceFiles()).hasSize(1); } @Test public void compilation_generatesExpectedIndexer() throws IOException { String expectedClassName = "GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule"; assertThat(compilation) .generatedSourceFile(annotation(expectedClassName)) .hasSourceEquivalentTo(forResource(expectedClassName + ".java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/GlideExtensionOptionsTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.emptyAppModule; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.SubDirectory; import com.bumptech.glide.annotation.compiler.test.TestDescription; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Verifies only the output we expect to change based on the various configurations of GlideOptions. */ @RunWith(JUnit4.class) public class GlideExtensionOptionsTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); @Rule public final TestDescription testDescription = new TestDescription(); private static final String EXTENSION_NAME = "Extension.java"; private Compilation currentCompilation; @Test @SubDirectory("OverrideExtend") public void compilation_withOverrideExtend_validOptions() throws IOException { runTest(Subject.GlideOptions); } @Test @SubDirectory("OverrideExtend") public void compilation_withOverrideExtend_validRequest() throws IOException { runTest(Subject.GlideRequest); } @Test @SubDirectory("OverrideExtendMultipleArguments") public void compilation_withOverrideReplace_andMultipleArguments_validOptions() throws IOException { runTest(Subject.GlideOptions); } @Test @SubDirectory("OverrideExtendMultipleArguments") public void compilation_withOverrideReplace_andMultipleArguments_validRequest() throws IOException { runTest(Subject.GlideRequest); } @Test @SubDirectory("OverrideReplace") public void compilation_withOverrideReplace_validOptions() throws IOException { runTest(Subject.GlideOptions); } @Test @SubDirectory("OverrideReplace") public void compilation_withOverrideReplace_validRequest() throws IOException { runTest(Subject.GlideRequest); } @Test @SubDirectory("StaticMethodName") public void compilation_withStaticMethodName_validOptions() throws IOException { runTest(Subject.GlideOptions); } @Test @SubDirectory("StaticMethodName") public void compilation_withStaticMethodName_validRequest() throws IOException { runTest(Subject.GlideRequest); } @Test @SubDirectory("MemoizeStaticMethod") public void compilation_withMemoizeStaticMethod_validOptions() throws IOException { runTest(Subject.GlideOptions); } @Test @SubDirectory("MemoizeStaticMethod") public void compilation_withMemoizeStaticMethod_validRequest() throws IOException { runTest(Subject.GlideRequest); } @Test @SubDirectory("SkipStaticMethod") public void compilation_withSkipStaticMethod_validOptions() throws IOException { runTest(Subject.GlideOptions); } @Test @SubDirectory("SkipStaticMethod") public void compilation_withSkipStaticMethod_validRequest() throws IOException { runTest(Subject.GlideRequest); } @Override public Compilation getCompilation() { return currentCompilation; } private enum Subject { GlideOptions, GlideRequest; String file() { return name() + ".java"; } } private void runTest(Subject subject) { String subDir = getSubDirectoryName(); currentCompilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile(emptyAppModule(), extension(subDir)); assertThat(currentCompilation).succeededWithoutWarnings(); assertThat(currentCompilation) .generatedSourceFile(subpackage(subject.name())) .hasSourceEquivalentTo(forResource(subDir, subject.file())); } private String getSubDirectoryName() { return testDescription.getDescription().getAnnotation(SubDirectory.class).value(); } private JavaFileObject extension(String subdir) { return forResource(subdir, EXTENSION_NAME); } private JavaFileObject forResource(String subdir, String name) { return Util.forResource(getClass().getSimpleName(), subdir + "/" + name); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/GlideExtensionWithOptionTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.appResource; import static com.bumptech.glide.annotation.compiler.test.Util.emptyAppModule; import static com.bumptech.glide.annotation.compiler.test.Util.glide; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.ReferencedResource; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Verifies the output of the processor with a simple single extension option in the new option * style where extension methods always return values. */ @RunWith(JUnit4.class) public class GlideExtensionWithOptionTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile(emptyAppModule(), forResource("ExtensionWithOption.java")); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_generatesAllExpectedFiles() { Truth.assertThat(compilation.generatedSourceFiles()).hasSize(7); } @Test public void compilation_generatesExpectedGlideOptionsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideOptions")) .hasSourceEquivalentTo(forResource("GlideOptions.java")); } @Test public void compilation_generatesExpectedGlideRequestClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequest")) .hasSourceEquivalentTo(forResource("GlideRequest.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequests")) .hasSourceEquivalentTo(appResource("GlideRequests.java")); } @Test @ReferencedResource public void compilationGeneratesExpectedGlideAppClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideApp")) .hasSourceEquivalentTo(appResource("GlideApp.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedAppGlideModuleImpl() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedAppGlideModuleImpl")) .hasSourceEquivalentTo(appResource("GeneratedAppGlideModuleImpl.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedRequestManagerFactory() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedRequestManagerFactory")) .hasSourceEquivalentTo(appResource("GeneratedRequestManagerFactory.java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/GlideExtensionWithTypeTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.appResource; import static com.bumptech.glide.annotation.compiler.test.Util.emptyAppModule; import static com.bumptech.glide.annotation.compiler.test.Util.glide; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.ReferencedResource; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Verifies the output of the processor with a simple single extension type. */ @RunWith(JUnit4.class) public class GlideExtensionWithTypeTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile(emptyAppModule(), forResource("ExtensionWithType.java")); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_generatesAllExpectedFiles() { Truth.assertThat(compilation.generatedSourceFiles()).hasSize(7); } @Test public void compilation_generatesExpectedGlideOptionsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideOptions")) .hasSourceEquivalentTo(forResource("GlideOptions.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGlideRequestClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequest")) .hasSourceEquivalentTo(appResource("GlideRequest.java")); } @Test public void compilation_generatesExpectedGlideRequestsClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideRequests")) .hasSourceEquivalentTo(forResource("GlideRequests.java")); } @Test @ReferencedResource public void compilationGeneratesExpectedGlideAppClass() throws IOException { assertThat(compilation) .generatedSourceFile(subpackage("GlideApp")) .hasSourceEquivalentTo(appResource("GlideApp.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedAppGlideModuleImpl() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedAppGlideModuleImpl")) .hasSourceEquivalentTo(appResource("GeneratedAppGlideModuleImpl.java")); } @Test @ReferencedResource public void compilation_generatesExpectedGeneratedRequestManagerFactory() throws IOException { assertThat(compilation) .generatedSourceFile(glide("GeneratedRequestManagerFactory")) .hasSourceEquivalentTo(appResource("GeneratedRequestManagerFactory.java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/InvalidAppGlideModuleWithExcludesTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import static org.junit.Assert.assertThrows; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests AppGlideModules with invalid usages of the @Excludes annotation. */ // Ignore warnings since most methods use assertThrows @SuppressWarnings("ResultOfMethodCallIgnored") @RunWith(JUnit4.class) public class InvalidAppGlideModuleWithExcludesTest { @Test public void compilation_withMissingExcludedModuleClass_throws() { assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( JavaFileObjects.forSourceLines( "AppModuleWithExcludes", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.Excludes;", "import com.bumptech.glide.annotation.GlideModule;", "import com.bumptech.glide.module.AppGlideModule;", "import com.bumptech.glide.test.EmptyLibraryModule;", "@GlideModule", "@Excludes(EmptyLibraryModule.class)", "public final class AppModuleWithExcludes extends AppGlideModule {}")); } }); } @Test public void compilation_withEmptyExcludes_fails() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( JavaFileObjects.forSourceLines( "AppModuleWithExcludes", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.Excludes;", "import com.bumptech.glide.annotation.GlideModule;", "import com.bumptech.glide.module.AppGlideModule;", "import com.bumptech.glide.test.EmptyLibraryModule;", "@GlideModule", "@Excludes", "public final class AppModuleWithExcludes extends AppGlideModule {}")); assertThat(compilation).failed(); } @Test public void compilation_withNonGlideModule_throws() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( JavaFileObjects.forSourceLines( "AppModuleWithExcludes", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.Excludes;", "import com.bumptech.glide.annotation.GlideModule;", "import com.bumptech.glide.module.AppGlideModule;", "import com.bumptech.glide.test.EmptyLibraryModule;", "@GlideModule", "@Excludes(Object.class)", "public final class AppModuleWithExcludes extends AppGlideModule {}")); assertThat(compilation).failed(); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/InvalidGlideExtensionTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.emptyAppModule; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import static org.junit.Assert.fail; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Checks assertions on {@link com.bumptech.glide.annotation.GlideExtension}s themselves. */ // Avoid warnings when asserting on exceptions. @SuppressWarnings("ResultOfMethodCallIgnored") @RunWith(JUnit4.class) public class InvalidGlideExtensionTest { @Test public void compilation_withPublicConstructor_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "PublicConstructor", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "@GlideExtension", "public class PublicConstructor { }")); fail("Failed to throw expected exception"); } catch (RuntimeException e) { Throwable cause = e.getCause(); Truth.assertThat(cause.getMessage()).contains("non-private constructor"); Truth.assertThat(cause.getMessage()).contains("PublicConstructor"); } } @Test public void compilation_withPackagePrivateExtension_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "PackagePrivateExtension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "@GlideExtension", "class PackagePrivateExtension {", " private PackagePrivateExtension() {}", "}")); fail("Failed to throw expected exception"); } catch (RuntimeException e) { Throwable cause = e.getCause(); Truth.assertThat(cause.getMessage()).contains("must be public"); Truth.assertThat(cause.getMessage()).contains("PackagePrivateExtension"); } } @Test public void compilation_withConstructorWithParameters_throws() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "ConstructorParametersExtension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "@GlideExtension", "public class ConstructorParametersExtension {", " private ConstructorParametersExtension(int failParam) {}", " public void doSomething() {}", "}")); fail("Failed to get expected exception"); } catch (RuntimeException e) { Throwable cause = e.getCause(); Truth.assertThat(cause.getMessage()).contains("parameters in the constructor"); Truth.assertThat(cause.getMessage()).contains("ConstructorParametersExtension"); } } @Test public void compilation_withNonStaticMethod_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "@GlideExtension", "public class Extension {", " private Extension() {}", " public void doSomething() {}", "}")); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_withStaticMethod_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "@GlideExtension", "public class Extension {", " private Extension() {}", " public static void doSomething() {}", "}")); assertThat(compilation).succeededWithoutWarnings(); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/InvalidGlideOptionsExtensionTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.emptyAppModule; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Checks assertions on {@link com.bumptech.glide.annotation.GlideExtension}s for methods annotated * with {@link com.bumptech.glide.annotation.GlideOption}. */ // Ignore warnings since most methods use assertThrows. @SuppressWarnings("ResultOfMethodCallIgnored") @RunWith(JUnit4.class) public class InvalidGlideOptionsExtensionTest { @Test public void compilation_withAnnotatedNonStaticMethod_fails() { assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption", " public void doSomething() {}", "}")); } }); } @Test public void compilation_withAnnotatedStaticMethod_withRequestOptionsArgInWrongOrder_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "NonRequestOptionsFirstArgExtension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class NonRequestOptionsFirstArgExtension{", " private NonRequestOptionsFirstArgExtension() {}", " @GlideOption", " public static BaseRequestOptions doSomething(", " Object arg1, BaseRequestOptions options) {", " return options;", " }", "}")); fail(); } catch (RuntimeException e) { String message = e.getCause().getMessage(); Truth.assertThat(message).contains("BaseRequestOptions object as their first parameter"); Truth.assertThat(message).contains("Object"); Truth.assertThat(message).contains("NonRequestOptionsFirstArgExtension"); } } @Test public void compilation_withAnnotatedStaticMethod_withRequestOptionsArg_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption", " public static BaseRequestOptions doSomething(", " BaseRequestOptions options) {", " return options;", " }", "}")); assertThat(compilation).succeeded(); } @Test public void compilation_withAnnotatedStaticMethod_withRequestOptionsArgAndOtherArg_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption", " public static BaseRequestOptions doSomething(", " BaseRequestOptions options, Object arg2) {", " return options;", " }", "}")); assertThat(compilation).succeeded(); } @Test public void compilation_overridingOptionWithoutAnnotationType_fails() { assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption", " public static BaseRequestOptions centerCrop(", " BaseRequestOptions options) {", " return options;", " }", "}")); } }); } @Test public void compilation_withOverrideExtend_butNotOverridingMethod_fails() { assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption(override = GlideOption.OVERRIDE_EXTEND)", " public static BaseRequestOptions something(", " BaseRequestOptions options) {", " return options;", " }", "}")); } }); } @Test public void compilation_withOverrideExtend_andOverridingMethod_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption(override = GlideOption.OVERRIDE_EXTEND)", " public static BaseRequestOptions centerCrop(", " BaseRequestOptions options) {", " return options;", " }", "}")); assertThat(compilation).succeeded(); } @Test public void compilation_withOverrideReplace_butNotOverridingMethod_fails() { assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption(override = GlideOption.OVERRIDE_REPLACE)", " public static BaseRequestOptions something(", " BaseRequestOptions options) {", " return options;", " }", "}")); } }); } @Test public void compilation_withOverrideReplace_andOverridingMethod_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption(override = GlideOption.OVERRIDE_REPLACE)", " public static BaseRequestOptions centerCrop(", " BaseRequestOptions options) {", " return options;", " }", "}")); assertThat(compilation).succeeded(); } @Test public void compilation_withRequestOptionsReturnValue_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @NonNull", " @GlideOption", " public static BaseRequestOptions doSomething(", " BaseRequestOptions options) {", " return options;", " }", "}")); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_withNonRequestOptionsReturnValue_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "WrongReturnTypeExtension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class WrongReturnTypeExtension {", " private WrongReturnTypeExtension() {}", " @NonNull", " @GlideOption", " public static Object doSomething(BaseRequestOptions options) {", " return options;", " }", "}")); fail(); } catch (RuntimeException e) { String message = e.getCause().getMessage(); Truth.assertThat(message) .contains("@GlideOption methods should return a BaseRequestOptions object"); Truth.assertThat(message).contains("Object"); Truth.assertThat(message).contains("WrongReturnTypeExtension"); } } @Test public void compilation_withMissingNonNullAnnotation_warns() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideOption", " public static BaseRequestOptions doSomething(", " BaseRequestOptions options) {", " return options;", " }", "}")); assertThat(compilation).succeeded(); assertThat(compilation).hadWarningCount(1); assertThat(compilation).hadWarningContaining("androidx.annotation.NonNull"); assertThat(compilation).hadWarningContaining("com.bumptech.glide.test.Extension#doSomething"); } @Test public void compilation_withNoOptionParameters_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "MissingRequestOptionsExtension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideOption;", "import com.bumptech.glide.request.BaseRequestOptions;", "@GlideExtension", "public class MissingRequestOptionsExtension {", " private MissingRequestOptionsExtension() {}", " @NonNull", " @GlideOption", " public static BaseRequestOptions doSomething() {", " return options;", " }", "}")); fail(); } catch (RuntimeException e) { String message = e.getCause().getMessage(); Truth.assertThat(message).contains("BaseRequestOptions object as their first parameter"); Truth.assertThat(message).contains("doSomething"); Truth.assertThat(message).contains("MissingRequestOptionsExtension"); } } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/InvalidGlideTypeExtensionTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.emptyAppModule; import static com.bumptech.glide.annotation.compiler.test.Util.subpackage; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Checks assertions on {@link com.bumptech.glide.annotation.GlideExtension}s for methods annotated * with {@link com.bumptech.glide.annotation.GlideType}. */ // Ignore warnings since most methods use assertThrows. @SuppressWarnings("ResultOfMethodCallIgnored") @RunWith(JUnit4.class) public class InvalidGlideTypeExtensionTest { @Test public void compilation_withAnnotatedNonStaticMethod_fails() { assertThrows( "@GlideType methods must be static", RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @NonNull", " @GlideType(Number.class)", " public RequestBuilder doSomething(", " RequestBuilder builder) {", " return builder;", " }", "}")); } }); } @Test public void compilation_withAnnotatedStaticMethod_withoutRequestBuilderArg_fails() { assertThrows( "@GlideType methods must take a RequestBuilder object as their first and only" + " parameter, but given multiple for:" + " com.bumptech.glide.test.Extension#doSomething()", RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideType(Number.class)", " public static RequestBuilder doSomething() {", " return null;", " }", "}")); } }); } @Test public void compilation_withAnnotatedStaticMethod_withRequestBuilderArg_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @NonNull", " @GlideType(Number.class)", " public static RequestBuilder type(RequestBuilder builder) {", " return builder;", " }", "}")); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_withAnnotatedStaticMethod_withNonRequestBuilderArg_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "WrongParameterTypeExtension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class WrongParameterTypeExtension {", " private WrongParameterTypeExtension() {}", " @NonNull", " @GlideType(Number.class)", " public static RequestBuilder type(Object arg) {", " return null;", " }", "}")); } catch (RuntimeException e) { String message = e.getCause().getMessage(); Truth.assertThat(message).contains("RequestBuilder object as their first and only parameter"); Truth.assertThat(message).contains("Object"); Truth.assertThat(message).contains("WrongParameterTypeExtension"); } } @Test public void compilation_withAnnotatedStaticMethod_withRequestBuilderArgAndOtherArg_fails() { assertThrows( "@GlideType methods must take a RequestBuilder object as their first and only" + " parameter, but given multiple for:" + " com.bumptech.glide.test.Extension#type(" + "com.bumptech.glide.RequestBuilder," + "java.lang.Object)", RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @NonNull", " @GlideType(Number.class)", " public static RequestBuilder type(", " RequestBuilder builder, Object arg2) {", " return builder;", " }", "}")); } }); } @Test public void compilation_withAnnotatedStaticMethod_overridingExistingType_fails() { final Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import android.graphics.drawable.Drawable;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @NonNull", " @GlideType(Drawable.class)", " public static RequestBuilder asDrawable(", " RequestBuilder builder) {", " return builder;", " }", "}")); assertThrows( "error: method asDrawable() is already defined in class" + " com.bumptech.glide.test.GlideRequests", RuntimeException.class, new ThrowingRunnable() { @Override public void run() { compilation.generatedSourceFile(subpackage("GlideRequests")); } }); } @Test public void compilation_withAnnotatedStaticMethod_returningRequestBuilder_succeeds() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @NonNull", " @GlideType(Number.class)", " public static RequestBuilder asNumber(", " RequestBuilder builder) {", " return builder;", " }", "}")); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_withAnnotatedStaticMethod_returningNonRequestBuilder_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "WrongReturnTypeExtension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class WrongReturnTypeExtension {", " private WrongReturnTypeExtension() {}", " @NonNull", " @GlideType(Number.class)", " public static Object asNumber(", " RequestBuilder builder) {", " return new Object();", " }", "}")); fail(); } catch (RuntimeException e) { String message = e.getCause().getMessage(); Truth.assertThat(message).contains("@GlideType methods should return a RequestBuilder"); Truth.assertThat(message).contains("Number"); Truth.assertThat(message).contains("WrongReturnTypeExtension"); } } @Test public void compilation_withAnnotatedStaticMethod_returningBuilderWithIncorrectType_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "WrongBuilderTypeExtension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class WrongBuilderTypeExtension {", " private WrongBuilderTypeExtension() {}", " @NonNull", " @GlideType(Number.class)", " public static RequestBuilder asNumber(", " RequestBuilder builder) {", " return builder;", " }", "}")); fail(); } catch (RuntimeException e) { String message = e.getCause().getMessage(); Truth.assertThat(message) .contains("@GlideType methods should return a RequestBuilder"); Truth.assertThat(message).contains("WrongBuilderTypeExtension"); } } @Test public void compilation_withAnnotatedStaticMethod_returningBuilder_andMultipleParams_fails() { assertThrows( "@GlideType methods must take a RequestBuilder object as their first and only parameter," + " but given multiple for:" + " com.bumptech.glide.test.Extension#asNumber(" + "com.bumptech.glide.RequestBuilder,java.lang.Object)", RuntimeException.class, new ThrowingRunnable() { @Override public void run() { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @NonNull", " @GlideType(Number.class)", " public static RequestBuilder asNumber(", " RequestBuilder builder, Object arg1) {", " return builder;", " }", "}")); } }); } @Test public void compilation_withAnnotatedStaticMethod_returningBuilder_nonBuilderParam_fails() { try { javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "IncorrectParameterExtension", "package com.bumptech.glide.test;", "import androidx.annotation.NonNull;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class IncorrectParameterExtension {", " private IncorrectParameterExtension() {}", " @NonNull", " @GlideType(Number.class)", " public static RequestBuilder asNumber(", " Object arg) {", " return null;", " }", "}")); fail(); } catch (RuntimeException e) { String message = e.getCause().getMessage(); Truth.assertThat(message) .contains( "@GlideType methods must take a RequestBuilder object" + " as their first and only parameter"); Truth.assertThat(message).contains("Object"); Truth.assertThat(message).contains("IncorrectParameterExtension"); } } @Test public void compilation_withAnnotatedStaticMethod_returningRequestBuilder_missingNonNull_warns() { Compilation compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( emptyAppModule(), JavaFileObjects.forSourceLines( "Extension", "package com.bumptech.glide.test;", "import com.bumptech.glide.RequestBuilder;", "import com.bumptech.glide.annotation.GlideExtension;", "import com.bumptech.glide.annotation.GlideType;", "@GlideExtension", "public class Extension {", " private Extension() {}", " @GlideType(Number.class)", " public static RequestBuilder asNumber(", " RequestBuilder builder) {", " return builder;", " }", "}")); assertThat(compilation).succeeded(); assertThat(compilation).hadWarningCount(1); assertThat(compilation).hadWarningContaining("androidx.annotation.NonNull"); assertThat(compilation).hadWarningContaining("com.bumptech.glide.test.Extension#asNumber"); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/MultipleAppGlideModuleTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import static org.junit.Assert.assertThrows; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.testing.compile.Compilation; import javax.tools.JavaFileObject; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Ensures that adding more than one {@link com.bumptech.glide.module.AppGlideModule} to a project * will fail. */ @RunWith(JUnit4.class) public class MultipleAppGlideModuleTest implements CompilationProvider { private static final String FIRST_MODULE = "EmptyAppModule1.java"; private static final String SECOND_MODULE = "EmptyAppModule2.java"; @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; // Throws. @SuppressWarnings("ResultOfMethodCallIgnored") @Test public void compilation_withTwoAppModules_fails() { assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { javac() .withProcessors(new GlideAnnotationProcessor()) .compile(forResource(FIRST_MODULE), forResource(SECOND_MODULE)); } }); } @Test public void compilation_withFirstModuleOnly_succeeds() { compilation = javac().withProcessors(new GlideAnnotationProcessor()).compile(forResource(FIRST_MODULE)); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_withSecondModuleOnly_succeeds() { compilation = javac().withProcessors(new GlideAnnotationProcessor()).compile(forResource(SECOND_MODULE)); assertThat(compilation).succeededWithoutWarnings(); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/MultipleEmptyLibraryGlideModuleTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.bumptech.glide.annotation.compiler.test.Util.annotation; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.bumptech.glide.annotation.compiler.test.RegenerateResourcesRule; import com.bumptech.glide.annotation.compiler.test.Util; import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests adding multiple {@link com.bumptech.glide.module.LibraryGlideModule}s in a project. */ @RunWith(JUnit4.class) public class MultipleEmptyLibraryGlideModuleTest implements CompilationProvider { @Rule public final RegenerateResourcesRule regenerateResourcesRule = new RegenerateResourcesRule(this); private Compilation compilation; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( forResource("EmptyLibraryModule1.java"), forResource("EmptyLibraryModule2.java")); assertThat(compilation).succeededWithoutWarnings(); } @Test public void compilation_generatesAllExpectedFiles() { Truth.assertThat(compilation.generatedSourceFiles()).hasSize(1); } @Test public void compilation_generatesExpectedIndexerForModules() throws IOException { String expectedClassName = "GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule1_com_bumptech_glide" + "_test_EmptyLibraryModule2"; assertThat(compilation) .generatedSourceFile(annotation(expectedClassName)) .hasSourceEquivalentTo(forResource(expectedClassName + ".java")); } private JavaFileObject forResource(String name) { return Util.forResource(getClass().getSimpleName(), name); } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/OverlyLongFileNameTest.java ================================================ package com.bumptech.glide.annotation.compiler; import static com.google.testing.compile.Compiler.javac; import com.bumptech.glide.annotation.compiler.test.CompilationProvider; import com.google.testing.compile.Compilation; import com.google.testing.compile.CompilationSubject; import com.google.testing.compile.JavaFileObjects; import java.io.File; import java.io.IOException; import javax.tools.JavaFileObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Makes sure that we can handle indexers based on long package or file names, or many modules. * *

See #4106. */ @RunWith(JUnit4.class) public class OverlyLongFileNameTest implements CompilationProvider { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); private Compilation compilation; private static final String FILE_NAME_LONGER_THAN_255_CHARS = "SomeReallyReallyRidiculouslyLongFileNameOrPackageNameIGuessThatExceedsTwoHundredAndFiftyFive" + "CharactersThoughThatsOnlyAroundOneHundredCharactersWhichMeansINeedToKeepTypingToGetTo" + "TwoHundredAndFiftyFiveSomehowThankfullyOnlyLikeFiftyToGoNowMaybeButNotQuiteYet" + "SomewhereAroundNowIsProbablyGood"; @Before public void setUp() { compilation = javac() .withProcessors(new GlideAnnotationProcessor()) .compile( JavaFileObjects.forSourceLines( FILE_NAME_LONGER_THAN_255_CHARS, "package com.bumptech.glide.test;", "import com.bumptech.glide.annotation.GlideModule;", "import com.bumptech.glide.module.LibraryGlideModule;", "@GlideModule", "public final class " + FILE_NAME_LONGER_THAN_255_CHARS + " extends LibraryGlideModule {}")); } @Test public void compilingLongClassAndOrPackageNameShouldSucceed() throws IOException { CompilationSubject.assertThat(compilation).succeededWithoutWarnings(); for (JavaFileObject file : compilation.generatedFiles()) { temporaryFolder.create(); String actualFileName = new File(file.getName()).getName(); if (!actualFileName.startsWith(FILE_NAME_LONGER_THAN_255_CHARS)) { try { temporaryFolder.newFile(actualFileName).createNewFile(); } catch (IOException e) { throw new RuntimeException("Failed to create: " + actualFileName, e); } } } } @Override public Compilation getCompilation() { return compilation; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/test/CompilationProvider.java ================================================ package com.bumptech.glide.annotation.compiler.test; import com.google.testing.compile.Compilation; /** Provides the {@link Compilation} used to compile test code. */ public interface CompilationProvider { Compilation getCompilation(); } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/test/ReferencedResource.java ================================================ package com.bumptech.glide.annotation.compiler.test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicates that the method in question is referencing a test resource that it doesn't "own" and * should not attempt to regenerate. * *

Used by {@link RegenerateResourcesRule} to ensure that if we are regenerating resources, we're * only regenerating them for a single class and only for the single class that has the correct name * and directory sequence so that we update the correct file. * *

Ideally this wouldn't be necessary. It would be great if we could find a way to go from the * test failure more directly to the actual path of the resource used. Right now we're basically * guessing based on this annotation, the class name of the test class, and any values from {@link * SubDirectory}. Without this annotation, we'd end up writing files that were never used. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ReferencedResource {} ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/test/RegenerateResourcesRule.java ================================================ package com.bumptech.glide.annotation.compiler.test; import static com.bumptech.glide.annotation.compiler.test.Util.asUnixChars; import androidx.annotation.NonNull; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import javax.tools.JavaFileObject; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; /** * Regenerates test resources for annotation compiler tests when the {@link * Util#REGENERATE_TEST_RESOURCES_PROPERTY_NAME} property is set to the directory containing the * project. * *

This can easily be used via gradle by running: {@code ./gradlew * :annotation:compiler:test:regenerateTestResources } * *

Our regenerate task will set the appropriate environment variables that will allow the logic * here to succeed. When running the tests normally, this class will do nothing. */ public final class RegenerateResourcesRule implements TestRule { private CompilationProvider test; public RegenerateResourcesRule(CompilationProvider test) { this.test = test; } @Override public Statement apply(final Statement base, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { base.evaluate(); } catch (AssertionError e) { String projectRoot = Util.getProjectRootIfRegeneratingTestResources(); if (projectRoot == null || description.getAnnotation(ReferencedResource.class) != null) { throw e; } updateResourceFile(e, projectRoot, description); } } }; } private void updateResourceFile( AssertionError e, @NonNull String projectDirectory, Description description) { String testClassName = test.getClass().getSimpleName(); String testFileName = parseFileNameFromMessage(e); String testDirectory = projectDirectory + "/src/test/resources/" + testClassName; String subDirectorySegment = description.getAnnotation(SubDirectory.class) != null ? description.getAnnotation(SubDirectory.class).value() + "/" : ""; File expectedDirectory = new File(testDirectory + "/" + subDirectorySegment); if (!expectedDirectory.exists() && !expectedDirectory.mkdirs()) { throw new IllegalStateException( "Failed to generate expected directory: " + expectedDirectory); } if (!expectedDirectory.isDirectory()) { throw new IllegalStateException( "Expected a directory, but found a file: " + expectedDirectory); } File expectedFile = new File(expectedDirectory, testFileName); Writer writer = null; try { writer = new FileWriter(expectedFile); writer.write(asUnixChars(parseActual(testFileName)).toString()); writer.close(); } catch (IOException e1) { throw new RuntimeException("Failed to regenerate test file", e1); } finally { if (writer != null) { try { writer.close(); } catch (IOException exception) { // Ignore. } } } } private String parseActual(String fileName) { for (JavaFileObject javaFileObject : test.getCompilation().generatedSourceFiles()) { if (javaFileObject.getName().contains(fileName)) { try { return javaFileObject.getCharContent(true).toString(); } catch (IOException e) { throw new IllegalStateException(e); } } } throw new IllegalStateException("Failed to find source file for name: " + fileName); } // Parses to GlideOptions.java. private static String parseFileNameFromMessage(AssertionError e) { String message = e.getMessage(); int firstGreaterThanIndex = message.indexOf('>'); String substring = message.substring(0, firstGreaterThanIndex); int lastForwardSlashIndex = substring.lastIndexOf('/'); return substring.substring(lastForwardSlashIndex + 1, substring.length()); } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/test/SubDirectory.java ================================================ package com.bumptech.glide.annotation.compiler.test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicates the subdirectory for a particular test that contains the test resource(s) used for the * method. * *

Used both by tests to extract the correct subdirectory and by the {@link * RegenerateResourcesRule} for the same purpose. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SubDirectory { String value(); } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/test/TestDescription.java ================================================ package com.bumptech.glide.annotation.compiler.test; import org.junit.rules.TestWatcher; import org.junit.runner.Description; /** * Exposes the {@link Description} for the current test, similar to {@link * org.junit.rules.TestName}. */ public final class TestDescription extends TestWatcher { private Description description; @Override protected void starting(Description description) { this.description = description; } public Description getDescription() { return description; } } ================================================ FILE: annotation/compiler/test/src/test/java/com/bumptech/glide/annotation/compiler/test/Util.java ================================================ package com.bumptech.glide.annotation.compiler.test; import com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; /** Test utilities. */ public final class Util { private static final String REGENERATE_TEST_RESOURCES_PROPERTY_NAME = "com.bumptech.glide.annotation.compiler.test.regenerate.path"; private static final String GLIDE_PACKAGE_NAME = "com.bumptech.glide"; private static final String SUB_PACKAGE_NAME = qualified(GLIDE_PACKAGE_NAME, "test"); private static final String ANNOTATION_PACKAGE_NAME = "com.bumptech.glide.annotation.compiler"; private static final String DEFAULT_APP_DIR_NAME = "EmptyAppGlideModuleTest"; private static final String DEFAULT_LIBRARY_DIR_NAME = "EmptyLibraryGlideModuleTest"; /** * Hardcoded file separator to workaround {@code JavaFileObjects.forResource(...)} defaulting to * the unix one. */ private static final String FILE_SEPARATOR = "/"; private static final String LINE_SEPARATOR = "\n"; private Util() { // Utility class. } /** * Returns the {@code String} from a system property that is expected to contain the project * directory for the module containing these tests or {@code null} if we're not currently * attempting to regenerate test resources. */ static String getProjectRootIfRegeneratingTestResources() { return System.getProperty(REGENERATE_TEST_RESOURCES_PROPERTY_NAME); } public static JavaFileObject emptyAppModule() { return appResource("EmptyAppModule.java"); } public static JavaFileObject emptyLibraryModule() { return libraryResource("EmptyLibraryModule.java"); } public static JavaFileObject appResource(String className) { return forResource(DEFAULT_APP_DIR_NAME, className); } public static JavaFileObject libraryResource(String className) { return forResource(DEFAULT_LIBRARY_DIR_NAME, className); } public static JavaFileObject forResource(String directoryName, String name) { try { return JavaFileObjects.forResource(directoryName + FILE_SEPARATOR + name); } catch (IllegalArgumentException e) { // IllegalArgumentException will be thrown if the resource is missing. If we're trying to // generate test resources for a new test, we want to avoid this exception because it does not // contain any expected output that we can write to a file. By returning an empty file, we // avoid the exception and get the output from our comparison tests that we can then write // out. // If we're not regenerating test resources, we should throw the normal exception. if (getProjectRootIfRegeneratingTestResources() != null) { return JavaFileObjects.forSourceString("com.bumptech.test.empty", ""); } throw e; } } public static String annotation(String className) { return qualified(ANNOTATION_PACKAGE_NAME, className); } public static String subpackage(String className) { return qualified(SUB_PACKAGE_NAME, className); } public static String glide(String className) { return qualified(GLIDE_PACKAGE_NAME, className); } public static CharSequence asUnixChars(CharSequence chars) { return chars.toString().replace(System.lineSeparator(), LINE_SEPARATOR); } private static String qualified(String packageName, String className) { return packageName + '.' + className; } } ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithExcludesTest/AppModuleWithExcludes.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule @Excludes(EmptyLibraryModule.class) public final class AppModuleWithExcludes extends AppGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithExcludesTest/GeneratedAppGlideModuleImpl.java ================================================ package com.bumptech.glide; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.test.AppModuleWithExcludes; import java.util.HashSet; import java.util.Set; @SuppressWarnings("deprecation") final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule { private final AppModuleWithExcludes appGlideModule; public GeneratedAppGlideModuleImpl(Context context) { appGlideModule = new AppModuleWithExcludes(); if (Log.isLoggable("Glide", Log.DEBUG)) { Log.d("Glide", "Discovered AppGlideModule from annotation: com.bumptech.glide.test.AppModuleWithExcludes"); Log.d("Glide", "AppGlideModule excludes LibraryGlideModule from annotation: com.bumptech.glide.test.EmptyLibraryModule"); } } @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { appGlideModule.applyOptions(context, builder); } @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { appGlideModule.registerComponents(context, glide, registry); } @Override public boolean isManifestParsingEnabled() { return appGlideModule.isManifestParsingEnabled(); } @Override @NonNull public Set> getExcludedModuleClasses() { Set> excludedClasses = new HashSet>(); excludedClasses.add(com.bumptech.glide.test.EmptyLibraryModule.class); return excludedClasses; } @Override @NonNull GeneratedRequestManagerFactory getRequestManagerFactory() { return new GeneratedRequestManagerFactory(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithLibraryInPackageTest/AppModuleWithLibraryInPackage.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.test._package.LibraryModuleInPackage; @GlideModule @Excludes(LibraryModuleInPackage.class) public final class AppModuleWithLibraryInPackage extends AppGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithLibraryInPackageTest/GeneratedAppGlideModuleImpl.java ================================================ package com.bumptech.glide; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.test.AppModuleWithLibraryInPackage; import java.util.HashSet; import java.util.Set; @SuppressWarnings("deprecation") final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule { private final AppModuleWithLibraryInPackage appGlideModule; public GeneratedAppGlideModuleImpl(Context context) { appGlideModule = new AppModuleWithLibraryInPackage(); if (Log.isLoggable("Glide", Log.DEBUG)) { Log.d("Glide", "Discovered AppGlideModule from annotation: com.bumptech.glide.test.AppModuleWithLibraryInPackage"); Log.d("Glide", "AppGlideModule excludes LibraryGlideModule from annotation: com.bumptech.glide.test._package.LibraryModuleInPackage"); } } @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { appGlideModule.applyOptions(context, builder); } @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { appGlideModule.registerComponents(context, glide, registry); } @Override public boolean isManifestParsingEnabled() { return appGlideModule.isManifestParsingEnabled(); } @Override @NonNull public Set> getExcludedModuleClasses() { Set> excludedClasses = new HashSet>(); excludedClasses.add(com.bumptech.glide.test._package.LibraryModuleInPackage.class); return excludedClasses; } @Override @NonNull GeneratedRequestManagerFactory getRequestManagerFactory() { return new GeneratedRequestManagerFactory(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithLibraryInPackageTest/LibraryModuleInPackage.java ================================================ // _ in the name is important otherwise everything would work package com.bumptech.glide.test._package; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public final class LibraryModuleInPackage extends LibraryGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithMultipleExcludesTest/AppModuleWithMultipleExcludes.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule @Excludes({EmptyLibraryModule1.class, EmptyLibraryModule2.class}) public final class AppModuleWithMultipleExcludes extends AppGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithMultipleExcludesTest/EmptyLibraryModule1.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public final class EmptyLibraryModule1 extends LibraryGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithMultipleExcludesTest/EmptyLibraryModule2.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public final class EmptyLibraryModule2 extends LibraryGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/AppGlideModuleWithMultipleExcludesTest/GeneratedAppGlideModuleImpl.java ================================================ package com.bumptech.glide; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.test.AppModuleWithMultipleExcludes; import java.util.HashSet; import java.util.Set; @SuppressWarnings("deprecation") final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule { private final AppModuleWithMultipleExcludes appGlideModule; public GeneratedAppGlideModuleImpl(Context context) { appGlideModule = new AppModuleWithMultipleExcludes(); if (Log.isLoggable("Glide", Log.DEBUG)) { Log.d("Glide", "Discovered AppGlideModule from annotation: com.bumptech.glide.test.AppModuleWithMultipleExcludes"); Log.d("Glide", "AppGlideModule excludes LibraryGlideModule from annotation: com.bumptech.glide.test.EmptyLibraryModule1"); Log.d("Glide", "AppGlideModule excludes LibraryGlideModule from annotation: com.bumptech.glide.test.EmptyLibraryModule2"); } } @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { appGlideModule.applyOptions(context, builder); } @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { appGlideModule.registerComponents(context, glide, registry); } @Override public boolean isManifestParsingEnabled() { return appGlideModule.isManifestParsingEnabled(); } @Override @NonNull public Set> getExcludedModuleClasses() { Set> excludedClasses = new HashSet>(); excludedClasses.add(com.bumptech.glide.test.EmptyLibraryModule1.class); excludedClasses.add(com.bumptech.glide.test.EmptyLibraryModule2.class); return excludedClasses; } @Override @NonNull GeneratedRequestManagerFactory getRequestManagerFactory() { return new GeneratedRequestManagerFactory(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppAndLibraryGlideModulesTest/GeneratedAppGlideModuleImpl.java ================================================ package com.bumptech.glide; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.test.EmptyAppModule; import com.bumptech.glide.test.EmptyLibraryModule; import java.util.Collections; import java.util.Set; @SuppressWarnings("deprecation") final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule { private final EmptyAppModule appGlideModule; public GeneratedAppGlideModuleImpl(Context context) { appGlideModule = new EmptyAppModule(); if (Log.isLoggable("Glide", Log.DEBUG)) { Log.d("Glide", "Discovered AppGlideModule from annotation: com.bumptech.glide.test.EmptyAppModule"); Log.d("Glide", "Discovered LibraryGlideModule from annotation: com.bumptech.glide.test.EmptyLibraryModule"); } } @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { appGlideModule.applyOptions(context, builder); } @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { new EmptyLibraryModule().registerComponents(context, glide, registry); appGlideModule.registerComponents(context, glide, registry); } @Override public boolean isManifestParsingEnabled() { return appGlideModule.isManifestParsingEnabled(); } @Override @NonNull public Set> getExcludedModuleClasses() { return Collections.emptySet(); } @Override @NonNull GeneratedRequestManagerFactory getRequestManagerFactory() { return new GeneratedRequestManagerFactory(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppGlideModuleTest/EmptyAppModule.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public final class EmptyAppModule extends AppGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppGlideModuleTest/GeneratedAppGlideModuleImpl.java ================================================ package com.bumptech.glide; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.test.EmptyAppModule; import java.util.Collections; import java.util.Set; @SuppressWarnings("deprecation") final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule { private final EmptyAppModule appGlideModule; public GeneratedAppGlideModuleImpl(Context context) { appGlideModule = new EmptyAppModule(); if (Log.isLoggable("Glide", Log.DEBUG)) { Log.d("Glide", "Discovered AppGlideModule from annotation: com.bumptech.glide.test.EmptyAppModule"); } } @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { appGlideModule.applyOptions(context, builder); } @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { appGlideModule.registerComponents(context, glide, registry); } @Override public boolean isManifestParsingEnabled() { return appGlideModule.isManifestParsingEnabled(); } @Override @NonNull public Set> getExcludedModuleClasses() { return Collections.emptySet(); } @Override @NonNull GeneratedRequestManagerFactory getRequestManagerFactory() { return new GeneratedRequestManagerFactory(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppGlideModuleTest/GeneratedRequestManagerFactory.java ================================================ package com.bumptech.glide; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.manager.Lifecycle; import com.bumptech.glide.manager.RequestManagerRetriever; import com.bumptech.glide.manager.RequestManagerTreeNode; import com.bumptech.glide.test.GlideRequests; /** * Generated code, do not modify */ final class GeneratedRequestManagerFactory implements RequestManagerRetriever.RequestManagerFactory { @Override @NonNull public RequestManager build(@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode treeNode, @NonNull Context context) { return new GlideRequests(glide, lifecycle, treeNode, context); } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppGlideModuleTest/GlideApp.java ================================================ package com.bumptech.glide.test; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import java.io.File; /** * The entry point for interacting with Glide for Applications * *

Includes all generated APIs from all * {@link com.bumptech.glide.annotation.GlideExtension}s in source and dependent libraries. * *

This class is generated and should not be modified * @see Glide */ public final class GlideApp { private GlideApp() { } /** * @see Glide#getPhotoCacheDir(Context) */ @Nullable public static File getPhotoCacheDir(@NonNull Context context) { return Glide.getPhotoCacheDir(context); } /** * @see Glide#getPhotoCacheDir(Context, String) */ @Nullable public static File getPhotoCacheDir(@NonNull Context context, @NonNull String string) { return Glide.getPhotoCacheDir(context, string); } /** * @see Glide#get(Context) */ @NonNull public static Glide get(@NonNull Context context) { return Glide.get(context); } /** * @see Glide#init(Glide) */ @Deprecated @VisibleForTesting @SuppressLint("VisibleForTests") public static void init(Glide glide) { Glide.init(glide); } /** * @see Glide#init(Context, GlideBuilder) */ @VisibleForTesting @SuppressLint("VisibleForTests") public static void init(@NonNull Context context, @NonNull GlideBuilder builder) { Glide.init(context, builder); } /** * @see Glide#enableHardwareBitmaps() */ @VisibleForTesting @SuppressLint("VisibleForTests") public static void enableHardwareBitmaps() { Glide.enableHardwareBitmaps(); } /** * @see Glide#tearDown() */ @VisibleForTesting @SuppressLint("VisibleForTests") public static void tearDown() { Glide.tearDown(); } /** * @see Glide#with(Context) */ @NonNull public static GlideRequests with(@NonNull Context context) { return (GlideRequests) Glide.with(context); } /** * @see Glide#with(Activity) */ @Deprecated @NonNull public static GlideRequests with(@NonNull Activity activity) { return (GlideRequests) Glide.with(activity); } /** * @see Glide#with(FragmentActivity) */ @NonNull public static GlideRequests with(@NonNull FragmentActivity activity) { return (GlideRequests) Glide.with(activity); } /** * @see Glide#with(Fragment) */ @NonNull public static GlideRequests with(@NonNull Fragment fragment) { return (GlideRequests) Glide.with(fragment); } /** * @see Glide#with(Fragment) */ @Deprecated @NonNull public static GlideRequests with(@NonNull android.app.Fragment fragment) { return (GlideRequests) Glide.with(fragment); } /** * @see Glide#with(View) */ @NonNull public static GlideRequests with(@NonNull View view) { return (GlideRequests) Glide.with(view); } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppGlideModuleTest/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions centerCrop() { return (GlideOptions) super.centerCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppGlideModuleTest/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int, int) */ @NonNull @CheckResult public GlideRequest override(int width, int height) { return (GlideRequest) super.override(width, height); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#centerCrop() */ @NonNull @CheckResult public GlideRequest centerCrop() { return (GlideRequest) super.centerCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyAppGlideModuleTest/GlideRequests.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.manager.Lifecycle; import com.bumptech.glide.manager.RequestManagerTreeNode; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import java.io.File; import java.net.URL; /** * Includes all additions from methods in {@link com.bumptech.glide.annotation.GlideExtension}s * annotated with {@link com.bumptech.glide.annotation.GlideType} * *

Generated code, do not modify */ @SuppressWarnings("deprecation") public class GlideRequests extends RequestManager { public GlideRequests(@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode treeNode, @NonNull Context context) { super(glide, lifecycle, treeNode, context); } @Override @CheckResult @NonNull public GlideRequest as(@NonNull Class resourceClass) { return new GlideRequest<>(glide, this, resourceClass, context); } @Override @NonNull public synchronized GlideRequests applyDefaultRequestOptions(@NonNull RequestOptions options) { return (GlideRequests) super.applyDefaultRequestOptions(options); } @Override @NonNull public synchronized GlideRequests setDefaultRequestOptions(@NonNull RequestOptions options) { return (GlideRequests) super.setDefaultRequestOptions(options); } @Override @NonNull public GlideRequests addDefaultRequestListener(RequestListener listener) { return (GlideRequests) super.addDefaultRequestListener(listener); } @Override @NonNull @CheckResult public GlideRequest asBitmap() { return (GlideRequest) super.asBitmap(); } @Override @NonNull @CheckResult public GlideRequest asGif() { return (GlideRequest) super.asGif(); } @Override @NonNull @CheckResult public GlideRequest asDrawable() { return (GlideRequest) super.asDrawable(); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest downloadOnly() { return (GlideRequest) super.downloadOnly(); } @Override @NonNull @CheckResult public GlideRequest download(@Nullable Object o) { return (GlideRequest) super.download(o); } @Override @NonNull @CheckResult public GlideRequest asFile() { return (GlideRequest) super.asFile(); } @Override protected void setRequestOptions(@NonNull RequestOptions toSet) { if (toSet instanceof com.bumptech.glide.test.GlideOptions) { super.setRequestOptions(toSet); } else { super.setRequestOptions(new com.bumptech.glide.test.GlideOptions().apply(toSet)); } } } ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyLibraryGlideModuleTest/EmptyLibraryModule.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public final class EmptyLibraryModule extends LibraryGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/EmptyLibraryGlideModuleTest/GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule.java ================================================ package com.bumptech.glide.annotation.compiler; @Index( modules = "com.bumptech.glide.test.EmptyLibraryModule" ) public class GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule { } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/MemoizeStaticMethod/Extension.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.request.BaseRequestOptions; @GlideExtension public final class Extension { private Extension() { // Utility class. } @NonNull @GlideOption(memoizeStaticMethod = true) public static BaseRequestOptions test(BaseRequestOptions requestOptions) { return requestOptions.centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/MemoizeStaticMethod/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see Extension */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform1; private static GlideOptions centerInsideTransform2; private static GlideOptions centerCropTransform3; private static GlideOptions circleCropTransform4; private static GlideOptions noTransformation5; private static GlideOptions noAnimation6; private static GlideOptions testOf0; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform1 == null) { GlideOptions.fitCenterTransform1 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform1; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform2 == null) { GlideOptions.centerInsideTransform2 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform2; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform3 == null) { GlideOptions.centerCropTransform3 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform3; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform4 == null) { GlideOptions.circleCropTransform4 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform4; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation5 == null) { GlideOptions.noTransformation5 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation5; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation6 == null) { GlideOptions.noAnimation6 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation6; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions centerCrop() { return (GlideOptions) super.centerCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } /** * @see Extension#test(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideOptions test() { return (GlideOptions) Extension.test(this); } /** * @see Extension#test(BaseRequestOptions) */ @CheckResult public static GlideOptions testOf() { if (GlideOptions.testOf0 == null) { GlideOptions.testOf0 = new GlideOptions().test().autoClone(); } return GlideOptions.testOf0; } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/MemoizeStaticMethod/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int, int) */ @NonNull @CheckResult public GlideRequest override(int width, int height) { return (GlideRequest) super.override(width, height); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#centerCrop() */ @NonNull @CheckResult public GlideRequest centerCrop() { return (GlideRequest) super.centerCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } /** * @see Extension#test(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideRequest test() { return (GlideRequest) Extension.test(this); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideExtend/Extension.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.request.BaseRequestOptions; @GlideExtension public final class Extension { private Extension() { // Utility class. } @NonNull @GlideOption(override = GlideOption.OVERRIDE_EXTEND) public static BaseRequestOptions centerCrop(BaseRequestOptions requestOptions) { return requestOptions.centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideExtend/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see Extension */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } /** * @see Extension#centerCrop(BaseRequestOptions) * @see GlideOptions#centerCrop() */ @SuppressWarnings("unchecked") @Override @CheckResult @NonNull public GlideOptions centerCrop() { return (GlideOptions) Extension.centerCrop(super.centerCrop()); } /** * @see Extension#centerCrop(BaseRequestOptions) */ @CheckResult public static GlideOptions centerCropOf() { return new GlideOptions().centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideExtend/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int, int) */ @NonNull @CheckResult public GlideRequest override(int width, int height) { return (GlideRequest) super.override(width, height); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } /** * @see Extension#centerCrop(BaseRequestOptions) * @see GlideRequest#centerCrop() */ @SuppressWarnings("unchecked") @Override @CheckResult @NonNull public GlideRequest centerCrop() { return (GlideRequest) Extension.centerCrop(super.centerCrop()); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideExtendMultipleArguments/Extension.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.request.BaseRequestOptions; @GlideExtension public final class Extension { private Extension() { // Utility class. } @NonNull @GlideOption(override = GlideOption.OVERRIDE_EXTEND) public static BaseRequestOptions override(BaseRequestOptions requestOptions, int width, int height) { return requestOptions .override(width, height) .centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideExtendMultipleArguments/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see Extension */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions centerCrop() { return (GlideOptions) super.centerCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } /** * @see Extension#override(BaseRequestOptions, int, int) * @see GlideOptions#override(int, int) */ @SuppressWarnings("unchecked") @Override @CheckResult @NonNull public GlideOptions override(int width, int height) { return (GlideOptions) Extension.override(super.override(width, height), width, height); } /** * @see Extension#override(BaseRequestOptions, int, int) */ @CheckResult public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideExtendMultipleArguments/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#centerCrop() */ @NonNull @CheckResult public GlideRequest centerCrop() { return (GlideRequest) super.centerCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } /** * @see Extension#override(BaseRequestOptions, int, int) * @see GlideRequest#override(int, int) */ @SuppressWarnings("unchecked") @Override @CheckResult @NonNull public GlideRequest override(int width, int height) { return (GlideRequest) Extension.override(super.override(width, height), width, height); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideReplace/Extension.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.request.BaseRequestOptions; @GlideExtension public final class Extension { private Extension() { // Utility class. } @NonNull @GlideOption(override = GlideOption.OVERRIDE_REPLACE) public static BaseRequestOptions centerCrop(BaseRequestOptions requestOptions) { return requestOptions.centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideReplace/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see Extension */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } /** * @see Extension#centerCrop(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideOptions centerCrop() { return (GlideOptions) Extension.centerCrop(this); } /** * @see Extension#centerCrop(BaseRequestOptions) */ @CheckResult public static GlideOptions centerCropOf() { return new GlideOptions().centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/OverrideReplace/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int, int) */ @NonNull @CheckResult public GlideRequest override(int width, int height) { return (GlideRequest) super.override(width, height); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } /** * @see Extension#centerCrop(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideRequest centerCrop() { return (GlideRequest) Extension.centerCrop(this); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/SkipStaticMethod/Extension.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.request.BaseRequestOptions; @GlideExtension public final class Extension { private Extension() { // Utility class. } @NonNull @GlideOption(skipStaticMethod = true) public static BaseRequestOptions test(BaseRequestOptions requestOptions) { return requestOptions.centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/SkipStaticMethod/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see Extension */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions centerCrop() { return (GlideOptions) super.centerCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } /** * @see Extension#test(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideOptions test() { return (GlideOptions) Extension.test(this); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/SkipStaticMethod/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int, int) */ @NonNull @CheckResult public GlideRequest override(int width, int height) { return (GlideRequest) super.override(width, height); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#centerCrop() */ @NonNull @CheckResult public GlideRequest centerCrop() { return (GlideRequest) super.centerCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } /** * @see Extension#test(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideRequest test() { return (GlideRequest) Extension.test(this); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/StaticMethodName/Extension.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.request.BaseRequestOptions; @GlideExtension public final class Extension { private Extension() { // Utility class. } @NonNull @GlideOption(staticMethodName = "testSomething") public static BaseRequestOptions test(BaseRequestOptions requestOptions) { return requestOptions.centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/StaticMethodName/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see Extension */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions centerCrop() { return (GlideOptions) super.centerCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } /** * @see Extension#test(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideOptions test() { return (GlideOptions) Extension.test(this); } /** * @see Extension#test(BaseRequestOptions) */ @CheckResult public static GlideOptions testSomething() { return new GlideOptions().test(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionOptionsTest/StaticMethodName/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int, int) */ @NonNull @CheckResult public GlideRequest override(int width, int height) { return (GlideRequest) super.override(width, height); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#centerCrop() */ @NonNull @CheckResult public GlideRequest centerCrop() { return (GlideRequest) super.centerCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } /** * @see Extension#test(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideRequest test() { return (GlideRequest) Extension.test(this); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionWithOptionTest/ExtensionWithOption.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideOption; import com.bumptech.glide.request.BaseRequestOptions; @GlideExtension public final class ExtensionWithOption { private ExtensionWithOption() { // Utility class. } @NonNull @GlideOption public static BaseRequestOptions squareThumb(BaseRequestOptions requestOptions) { return requestOptions.centerCrop(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionWithOptionTest/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see ExtensionWithOption */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions centerCrop() { return (GlideOptions) super.centerCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } /** * @see ExtensionWithOption#squareThumb(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideOptions squareThumb() { return (GlideOptions) ExtensionWithOption.squareThumb(this); } /** * @see ExtensionWithOption#squareThumb(BaseRequestOptions) */ @CheckResult public static GlideOptions squareThumbOf() { return new GlideOptions().squareThumb(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionWithOptionTest/GlideRequest.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import java.io.File; import java.net.URL; import java.util.List; /** * Contains all public methods from {@link RequestBuilder}, all options from * {@link com.bumptech.glide.request.RequestOptions} and all generated options from * {@link com.bumptech.glide.annotation.GlideOption} in annotated methods in * {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * *

Generated code, do not modify. * * @see RequestBuilder * @see com.bumptech.glide.request.RequestOptions */ @SuppressWarnings({ "unused", "deprecation" }) public class GlideRequest extends RequestBuilder implements Cloneable { GlideRequest(@NonNull Class transcodeClass, @NonNull RequestBuilder other) { super(transcodeClass, other); } GlideRequest(@NonNull Glide glide, @NonNull RequestManager requestManager, @NonNull Class transcodeClass, @NonNull Context context) { super(glide, requestManager ,transcodeClass, context); } @Override @CheckResult @NonNull protected GlideRequest getDownloadOnlyRequest() { return new GlideRequest<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } /** * @see GlideOptions#sizeMultiplier(float) */ @NonNull @CheckResult public GlideRequest sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideRequest) super.sizeMultiplier(value); } /** * @see GlideOptions#useUnlimitedSourceGeneratorsPool(boolean) */ @NonNull @CheckResult public GlideRequest useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideRequest) super.useUnlimitedSourceGeneratorsPool(flag); } /** * @see GlideOptions#useAnimationPool(boolean) */ @NonNull @CheckResult public GlideRequest useAnimationPool(boolean flag) { return (GlideRequest) super.useAnimationPool(flag); } /** * @see GlideOptions#onlyRetrieveFromCache(boolean) */ @NonNull @CheckResult public GlideRequest onlyRetrieveFromCache(boolean flag) { return (GlideRequest) super.onlyRetrieveFromCache(flag); } /** * @see GlideOptions#diskCacheStrategy(DiskCacheStrategy) */ @NonNull @CheckResult public GlideRequest diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideRequest) super.diskCacheStrategy(strategy); } /** * @see GlideOptions#priority(Priority) */ @NonNull @CheckResult public GlideRequest priority(@NonNull Priority priority) { return (GlideRequest) super.priority(priority); } /** * @see GlideOptions#placeholder(Drawable) */ @NonNull @CheckResult public GlideRequest placeholder(@Nullable Drawable drawable) { return (GlideRequest) super.placeholder(drawable); } /** * @see GlideOptions#placeholder(int) */ @NonNull @CheckResult public GlideRequest placeholder(@DrawableRes int id) { return (GlideRequest) super.placeholder(id); } /** * @see GlideOptions#fallback(Drawable) */ @NonNull @CheckResult public GlideRequest fallback(@Nullable Drawable drawable) { return (GlideRequest) super.fallback(drawable); } /** * @see GlideOptions#fallback(int) */ @NonNull @CheckResult public GlideRequest fallback(@DrawableRes int id) { return (GlideRequest) super.fallback(id); } /** * @see GlideOptions#error(Drawable) */ @NonNull @CheckResult public GlideRequest error(@Nullable Drawable drawable) { return (GlideRequest) super.error(drawable); } /** * @see GlideOptions#error(int) */ @NonNull @CheckResult public GlideRequest error(@DrawableRes int id) { return (GlideRequest) super.error(id); } /** * @see GlideOptions#theme(Resources.Theme) */ @NonNull @CheckResult public GlideRequest theme(@Nullable Resources.Theme theme) { return (GlideRequest) super.theme(theme); } /** * @see GlideOptions#skipMemoryCache(boolean) */ @NonNull @CheckResult public GlideRequest skipMemoryCache(boolean skip) { return (GlideRequest) super.skipMemoryCache(skip); } /** * @see GlideOptions#override(int, int) */ @NonNull @CheckResult public GlideRequest override(int width, int height) { return (GlideRequest) super.override(width, height); } /** * @see GlideOptions#override(int) */ @NonNull @CheckResult public GlideRequest override(int size) { return (GlideRequest) super.override(size); } /** * @see GlideOptions#signature(Key) */ @NonNull @CheckResult public GlideRequest signature(@NonNull Key key) { return (GlideRequest) super.signature(key); } /** * @see GlideOptions#set(Option, Y) */ @NonNull @CheckResult public GlideRequest set(@NonNull Option option, @NonNull Y y) { return (GlideRequest) super.set(option, y); } /** * @see GlideOptions#decode(Class) */ @NonNull @CheckResult public GlideRequest decode(@NonNull Class clazz) { return (GlideRequest) super.decode(clazz); } /** * @see GlideOptions#encodeFormat(Bitmap.CompressFormat) */ @NonNull @CheckResult public GlideRequest encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideRequest) super.encodeFormat(format); } /** * @see GlideOptions#encodeQuality(int) */ @NonNull @CheckResult public GlideRequest encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideRequest) super.encodeQuality(value); } /** * @see GlideOptions#frame(long) */ @NonNull @CheckResult public GlideRequest frame(@IntRange(from = 0) long value) { return (GlideRequest) super.frame(value); } /** * @see GlideOptions#format(DecodeFormat) */ @NonNull @CheckResult public GlideRequest format(@NonNull DecodeFormat format) { return (GlideRequest) super.format(format); } /** * @see GlideOptions#disallowHardwareConfig() */ @NonNull @CheckResult public GlideRequest disallowHardwareConfig() { return (GlideRequest) super.disallowHardwareConfig(); } /** * @see GlideOptions#downsample(DownsampleStrategy) */ @NonNull @CheckResult public GlideRequest downsample(@NonNull DownsampleStrategy strategy) { return (GlideRequest) super.downsample(strategy); } /** * @see GlideOptions#timeout(int) */ @NonNull @CheckResult public GlideRequest timeout(@IntRange(from = 0) int value) { return (GlideRequest) super.timeout(value); } /** * @see GlideOptions#optionalCenterCrop() */ @NonNull @CheckResult public GlideRequest optionalCenterCrop() { return (GlideRequest) super.optionalCenterCrop(); } /** * @see GlideOptions#centerCrop() */ @NonNull @CheckResult public GlideRequest centerCrop() { return (GlideRequest) super.centerCrop(); } /** * @see GlideOptions#optionalFitCenter() */ @NonNull @CheckResult public GlideRequest optionalFitCenter() { return (GlideRequest) super.optionalFitCenter(); } /** * @see GlideOptions#fitCenter() */ @NonNull @CheckResult public GlideRequest fitCenter() { return (GlideRequest) super.fitCenter(); } /** * @see GlideOptions#optionalCenterInside() */ @NonNull @CheckResult public GlideRequest optionalCenterInside() { return (GlideRequest) super.optionalCenterInside(); } /** * @see GlideOptions#centerInside() */ @NonNull @CheckResult public GlideRequest centerInside() { return (GlideRequest) super.centerInside(); } /** * @see GlideOptions#optionalCircleCrop() */ @NonNull @CheckResult public GlideRequest optionalCircleCrop() { return (GlideRequest) super.optionalCircleCrop(); } /** * @see GlideOptions#circleCrop() */ @NonNull @CheckResult public GlideRequest circleCrop() { return (GlideRequest) super.circleCrop(); } /** * @see GlideOptions#transform(Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Transformation transformation) { return (GlideRequest) super.transform(transformation); } /** * @see GlideOptions#transform(Transformation[]) */ @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transform(@NonNull Transformation... transformations) { return (GlideRequest) super.transform(transformations); } /** * @see GlideOptions#transforms(Transformation[]) */ @Deprecated @NonNull @CheckResult @SuppressWarnings({ "unchecked", "varargs" }) public GlideRequest transforms( @NonNull Transformation... transformations) { return (GlideRequest) super.transforms(transformations); } /** * @see GlideOptions#optionalTransform(Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform( @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(transformation); } /** * @see GlideOptions#optionalTransform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.optionalTransform(clazz, transformation); } /** * @see GlideOptions#transform(Class, Transformation) */ @NonNull @CheckResult public GlideRequest transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideRequest) super.transform(clazz, transformation); } /** * @see GlideOptions#dontTransform() */ @NonNull @CheckResult public GlideRequest dontTransform() { return (GlideRequest) super.dontTransform(); } /** * @see GlideOptions#dontAnimate() */ @NonNull @CheckResult public GlideRequest dontAnimate() { return (GlideRequest) super.dontAnimate(); } /** * @see GlideOptions#lock() */ @NonNull public GlideRequest lock() { return (GlideRequest) super.lock(); } /** * @see GlideOptions#autoClone() */ @NonNull public GlideRequest autoClone() { return (GlideRequest) super.autoClone(); } @Override @NonNull @CheckResult public GlideRequest apply(@NonNull BaseRequestOptions options) { return (GlideRequest) super.apply(options); } @Override @NonNull @CheckResult public GlideRequest transition( @NonNull TransitionOptions options) { return (GlideRequest) super.transition(options); } @Override @NonNull @CheckResult public GlideRequest listener(@Nullable RequestListener listener) { return (GlideRequest) super.listener(listener); } @Override @NonNull @CheckResult public GlideRequest addListener( @Nullable RequestListener listener) { return (GlideRequest) super.addListener(listener); } @Override @NonNull public GlideRequest error(@Nullable RequestBuilder builder) { return (GlideRequest) super.error(builder); } @Override @NonNull @CheckResult public GlideRequest error(Object o) { return (GlideRequest) super.error(o); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable RequestBuilder builder) { return (GlideRequest) super.thumbnail(builder); } @Override @NonNull @CheckResult @SafeVarargs @SuppressWarnings("varargs") public final GlideRequest thumbnail( @Nullable RequestBuilder... builders) { return (GlideRequest) super.thumbnail(builders); } @Override @NonNull @CheckResult public GlideRequest thumbnail(@Nullable List> list) { return (GlideRequest) super.thumbnail(list); } @Override @Deprecated @NonNull @CheckResult public GlideRequest thumbnail(float sizeMultiplier) { return (GlideRequest) super.thumbnail(sizeMultiplier); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @CheckResult public GlideRequest clone() { return (GlideRequest) super.clone(); } /** * @see ExtensionWithOption#squareThumb(BaseRequestOptions) */ @SuppressWarnings("unchecked") @CheckResult @NonNull public GlideRequest squareThumb() { return (GlideRequest) ExtensionWithOption.squareThumb(this); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionWithTypeTest/ExtensionWithType.java ================================================ package com.bumptech.glide.test; import androidx.annotation.NonNull; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.annotation.GlideExtension; import com.bumptech.glide.annotation.GlideType; @GlideExtension public final class ExtensionWithType { private ExtensionWithType() { // Utility class. } @NonNull @GlideType(Number.class) public static RequestBuilder asNumber(RequestBuilder builder) { return builder; } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionWithTypeTest/GlideOptions.java ================================================ package com.bumptech.glide.test; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestOptions; /** * Automatically generated from {@link com.bumptech.glide.annotation.GlideExtension} annotated classes. * * @see RequestOptions * @see ExtensionWithType */ @SuppressWarnings("deprecation") public final class GlideOptions extends RequestOptions implements Cloneable { private static GlideOptions fitCenterTransform0; private static GlideOptions centerInsideTransform1; private static GlideOptions centerCropTransform2; private static GlideOptions circleCropTransform3; private static GlideOptions noTransformation4; private static GlideOptions noAnimation5; /** * @see RequestOptions#sizeMultiplierOf(float) */ @CheckResult @NonNull public static GlideOptions sizeMultiplierOf(@FloatRange(from = 0.0, to = 1.0) float value) { return new GlideOptions().sizeMultiplier(value); } /** * @see RequestOptions#diskCacheStrategyOf(DiskCacheStrategy) */ @CheckResult @NonNull public static GlideOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy strategy) { return new GlideOptions().diskCacheStrategy(strategy); } /** * @see RequestOptions#priorityOf(Priority) */ @CheckResult @NonNull public static GlideOptions priorityOf(@NonNull Priority priority) { return new GlideOptions().priority(priority); } /** * @see RequestOptions#placeholderOf(Drawable) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@Nullable Drawable drawable) { return new GlideOptions().placeholder(drawable); } /** * @see RequestOptions#placeholderOf(int) */ @CheckResult @NonNull public static GlideOptions placeholderOf(@DrawableRes int id) { return new GlideOptions().placeholder(id); } /** * @see RequestOptions#errorOf(Drawable) */ @CheckResult @NonNull public static GlideOptions errorOf(@Nullable Drawable drawable) { return new GlideOptions().error(drawable); } /** * @see RequestOptions#errorOf(int) */ @CheckResult @NonNull public static GlideOptions errorOf(@DrawableRes int id) { return new GlideOptions().error(id); } /** * @see RequestOptions#skipMemoryCacheOf(boolean) */ @CheckResult @NonNull public static GlideOptions skipMemoryCacheOf(boolean skipMemoryCache) { return new GlideOptions().skipMemoryCache(skipMemoryCache); } /** * @see RequestOptions#overrideOf(int, int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int width, int height) { return new GlideOptions().override(width, height); } /** * @see RequestOptions#overrideOf(int) */ @CheckResult @NonNull public static GlideOptions overrideOf(int size) { return new GlideOptions().override(size); } /** * @see RequestOptions#signatureOf(Key) */ @CheckResult @NonNull public static GlideOptions signatureOf(@NonNull Key key) { return new GlideOptions().signature(key); } /** * @see RequestOptions#fitCenterTransform() */ @CheckResult @NonNull public static GlideOptions fitCenterTransform() { if (GlideOptions.fitCenterTransform0 == null) { GlideOptions.fitCenterTransform0 = new GlideOptions().fitCenter().autoClone(); } return GlideOptions.fitCenterTransform0; } /** * @see RequestOptions#centerInsideTransform() */ @CheckResult @NonNull public static GlideOptions centerInsideTransform() { if (GlideOptions.centerInsideTransform1 == null) { GlideOptions.centerInsideTransform1 = new GlideOptions().centerInside().autoClone(); } return GlideOptions.centerInsideTransform1; } /** * @see RequestOptions#centerCropTransform() */ @CheckResult @NonNull public static GlideOptions centerCropTransform() { if (GlideOptions.centerCropTransform2 == null) { GlideOptions.centerCropTransform2 = new GlideOptions().centerCrop().autoClone(); } return GlideOptions.centerCropTransform2; } /** * @see RequestOptions#circleCropTransform() */ @CheckResult @NonNull public static GlideOptions circleCropTransform() { if (GlideOptions.circleCropTransform3 == null) { GlideOptions.circleCropTransform3 = new GlideOptions().circleCrop().autoClone(); } return GlideOptions.circleCropTransform3; } /** * @see RequestOptions#bitmapTransform(Transformation) */ @CheckResult @NonNull public static GlideOptions bitmapTransform(@NonNull Transformation transformation) { return new GlideOptions().transform(transformation); } /** * @see RequestOptions#noTransformation() */ @CheckResult @NonNull public static GlideOptions noTransformation() { if (GlideOptions.noTransformation4 == null) { GlideOptions.noTransformation4 = new GlideOptions().dontTransform().autoClone(); } return GlideOptions.noTransformation4; } /** * @see RequestOptions#option(Option, T) */ @CheckResult @NonNull public static GlideOptions option(@NonNull Option option, @NonNull T t) { return new GlideOptions().set(option, t); } /** * @see RequestOptions#decodeTypeOf(Class) */ @CheckResult @NonNull public static GlideOptions decodeTypeOf(@NonNull Class clazz) { return new GlideOptions().decode(clazz); } /** * @see RequestOptions#formatOf(DecodeFormat) */ @CheckResult @NonNull public static GlideOptions formatOf(@NonNull DecodeFormat format) { return new GlideOptions().format(format); } /** * @see RequestOptions#frameOf(long) */ @CheckResult @NonNull public static GlideOptions frameOf(@IntRange(from = 0) long value) { return new GlideOptions().frame(value); } /** * @see RequestOptions#downsampleOf(DownsampleStrategy) */ @CheckResult @NonNull public static GlideOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new GlideOptions().downsample(strategy); } /** * @see RequestOptions#timeoutOf(int) */ @CheckResult @NonNull public static GlideOptions timeoutOf(@IntRange(from = 0) int value) { return new GlideOptions().timeout(value); } /** * @see RequestOptions#encodeQualityOf(int) */ @CheckResult @NonNull public static GlideOptions encodeQualityOf(@IntRange(from = 0, to = 100) int value) { return new GlideOptions().encodeQuality(value); } /** * @see RequestOptions#encodeFormatOf(CompressFormat) */ @CheckResult @NonNull public static GlideOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new GlideOptions().encodeFormat(format); } /** * @see RequestOptions#noAnimation() */ @CheckResult @NonNull public static GlideOptions noAnimation() { if (GlideOptions.noAnimation5 == null) { GlideOptions.noAnimation5 = new GlideOptions().dontAnimate().autoClone(); } return GlideOptions.noAnimation5; } @Override @NonNull @CheckResult public GlideOptions sizeMultiplier(@FloatRange(from = 0.0, to = 1.0) float value) { return (GlideOptions) super.sizeMultiplier(value); } @Override @NonNull @CheckResult public GlideOptions useUnlimitedSourceGeneratorsPool(boolean flag) { return (GlideOptions) super.useUnlimitedSourceGeneratorsPool(flag); } @Override @NonNull @CheckResult public GlideOptions useAnimationPool(boolean flag) { return (GlideOptions) super.useAnimationPool(flag); } @Override @NonNull @CheckResult public GlideOptions onlyRetrieveFromCache(boolean flag) { return (GlideOptions) super.onlyRetrieveFromCache(flag); } @Override @NonNull @CheckResult public GlideOptions diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { return (GlideOptions) super.diskCacheStrategy(strategy); } @Override @NonNull @CheckResult public GlideOptions priority(@NonNull Priority priority) { return (GlideOptions) super.priority(priority); } @Override @NonNull @CheckResult public GlideOptions placeholder(@Nullable Drawable drawable) { return (GlideOptions) super.placeholder(drawable); } @Override @NonNull @CheckResult public GlideOptions placeholder(@DrawableRes int id) { return (GlideOptions) super.placeholder(id); } @Override @NonNull @CheckResult public GlideOptions fallback(@Nullable Drawable drawable) { return (GlideOptions) super.fallback(drawable); } @Override @NonNull @CheckResult public GlideOptions fallback(@DrawableRes int id) { return (GlideOptions) super.fallback(id); } @Override @NonNull @CheckResult public GlideOptions error(@Nullable Drawable drawable) { return (GlideOptions) super.error(drawable); } @Override @NonNull @CheckResult public GlideOptions error(@DrawableRes int id) { return (GlideOptions) super.error(id); } @Override @NonNull @CheckResult public GlideOptions theme(@Nullable Resources.Theme theme) { return (GlideOptions) super.theme(theme); } @Override @NonNull @CheckResult public GlideOptions skipMemoryCache(boolean skip) { return (GlideOptions) super.skipMemoryCache(skip); } @Override @NonNull @CheckResult public GlideOptions override(int width, int height) { return (GlideOptions) super.override(width, height); } @Override @NonNull @CheckResult public GlideOptions override(int size) { return (GlideOptions) super.override(size); } @Override @NonNull @CheckResult public GlideOptions signature(@NonNull Key key) { return (GlideOptions) super.signature(key); } @Override @CheckResult public GlideOptions clone() { return (GlideOptions) super.clone(); } @Override @NonNull @CheckResult public GlideOptions set(@NonNull Option option, @NonNull Y y) { return (GlideOptions) super.set(option, y); } @Override @NonNull @CheckResult public GlideOptions decode(@NonNull Class clazz) { return (GlideOptions) super.decode(clazz); } @Override @NonNull @CheckResult public GlideOptions encodeFormat(@NonNull Bitmap.CompressFormat format) { return (GlideOptions) super.encodeFormat(format); } @Override @NonNull @CheckResult public GlideOptions encodeQuality(@IntRange(from = 0, to = 100) int value) { return (GlideOptions) super.encodeQuality(value); } @Override @NonNull @CheckResult public GlideOptions frame(@IntRange(from = 0) long value) { return (GlideOptions) super.frame(value); } @Override @NonNull @CheckResult public GlideOptions format(@NonNull DecodeFormat format) { return (GlideOptions) super.format(format); } @Override @NonNull @CheckResult public GlideOptions disallowHardwareConfig() { return (GlideOptions) super.disallowHardwareConfig(); } @Override @NonNull @CheckResult public GlideOptions downsample(@NonNull DownsampleStrategy strategy) { return (GlideOptions) super.downsample(strategy); } @Override @NonNull @CheckResult public GlideOptions timeout(@IntRange(from = 0) int value) { return (GlideOptions) super.timeout(value); } @Override @NonNull @CheckResult public GlideOptions optionalCenterCrop() { return (GlideOptions) super.optionalCenterCrop(); } @Override @NonNull @CheckResult public GlideOptions centerCrop() { return (GlideOptions) super.centerCrop(); } @Override @NonNull @CheckResult public GlideOptions optionalFitCenter() { return (GlideOptions) super.optionalFitCenter(); } @Override @NonNull @CheckResult public GlideOptions fitCenter() { return (GlideOptions) super.fitCenter(); } @Override @NonNull @CheckResult public GlideOptions optionalCenterInside() { return (GlideOptions) super.optionalCenterInside(); } @Override @NonNull @CheckResult public GlideOptions centerInside() { return (GlideOptions) super.centerInside(); } @Override @NonNull @CheckResult public GlideOptions optionalCircleCrop() { return (GlideOptions) super.optionalCircleCrop(); } @Override @NonNull @CheckResult public GlideOptions circleCrop() { return (GlideOptions) super.circleCrop(); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Transformation transformation) { return (GlideOptions) super.transform(transformation); } @Override @SafeVarargs @SuppressWarnings("varargs") @NonNull @CheckResult public final GlideOptions transform(@NonNull Transformation... transformations) { return (GlideOptions) super.transform(transformations); } @Override @SafeVarargs @SuppressWarnings("varargs") @Deprecated @NonNull @CheckResult public final GlideOptions transforms(@NonNull Transformation... transformations) { return (GlideOptions) super.transforms(transformations); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(transformation); } @Override @NonNull @CheckResult public GlideOptions optionalTransform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.optionalTransform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions transform(@NonNull Class clazz, @NonNull Transformation transformation) { return (GlideOptions) super.transform(clazz, transformation); } @Override @NonNull @CheckResult public GlideOptions dontTransform() { return (GlideOptions) super.dontTransform(); } @Override @NonNull @CheckResult public GlideOptions dontAnimate() { return (GlideOptions) super.dontAnimate(); } @Override @NonNull @CheckResult public GlideOptions apply(@NonNull BaseRequestOptions options) { return (GlideOptions) super.apply(options); } @Override @NonNull public GlideOptions lock() { return (GlideOptions) super.lock(); } @Override @NonNull public GlideOptions autoClone() { return (GlideOptions) super.autoClone(); } } ================================================ FILE: annotation/compiler/test/src/test/resources/GlideExtensionWithTypeTest/GlideRequests.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.manager.Lifecycle; import com.bumptech.glide.manager.RequestManagerTreeNode; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import java.io.File; import java.net.URL; /** * Includes all additions from methods in {@link com.bumptech.glide.annotation.GlideExtension}s * annotated with {@link com.bumptech.glide.annotation.GlideType} * *

Generated code, do not modify */ @SuppressWarnings("deprecation") public class GlideRequests extends RequestManager { public GlideRequests(@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode treeNode, @NonNull Context context) { super(glide, lifecycle, treeNode, context); } @Override @CheckResult @NonNull public GlideRequest as(@NonNull Class resourceClass) { return new GlideRequest<>(glide, this, resourceClass, context); } /** * @see ExtensionWithType#asNumber(RequestBuilder) */ @NonNull @CheckResult public GlideRequest asNumber() { return (GlideRequest) ExtensionWithType.asNumber(this.as(Number.class)); } @Override @NonNull public synchronized GlideRequests applyDefaultRequestOptions(@NonNull RequestOptions options) { return (GlideRequests) super.applyDefaultRequestOptions(options); } @Override @NonNull public synchronized GlideRequests setDefaultRequestOptions(@NonNull RequestOptions options) { return (GlideRequests) super.setDefaultRequestOptions(options); } @Override @NonNull public GlideRequests addDefaultRequestListener(RequestListener listener) { return (GlideRequests) super.addDefaultRequestListener(listener); } @Override @NonNull @CheckResult public GlideRequest asBitmap() { return (GlideRequest) super.asBitmap(); } @Override @NonNull @CheckResult public GlideRequest asGif() { return (GlideRequest) super.asGif(); } @Override @NonNull @CheckResult public GlideRequest asDrawable() { return (GlideRequest) super.asDrawable(); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Bitmap bitmap) { return (GlideRequest) super.load(bitmap); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Drawable drawable) { return (GlideRequest) super.load(drawable); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable String string) { return (GlideRequest) super.load(string); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Uri uri) { return (GlideRequest) super.load(uri); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable File file) { return (GlideRequest) super.load(file); } @Override @NonNull @CheckResult public GlideRequest load(@RawRes @DrawableRes @Nullable Integer id) { return (GlideRequest) super.load(id); } @Override @Deprecated @CheckResult public GlideRequest load(@Nullable URL url) { return (GlideRequest) super.load(url); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable byte[] bytes) { return (GlideRequest) super.load(bytes); } @Override @NonNull @CheckResult public GlideRequest load(@Nullable Object o) { return (GlideRequest) super.load(o); } @Override @NonNull @CheckResult public GlideRequest downloadOnly() { return (GlideRequest) super.downloadOnly(); } @Override @NonNull @CheckResult public GlideRequest download(@Nullable Object o) { return (GlideRequest) super.download(o); } @Override @NonNull @CheckResult public GlideRequest asFile() { return (GlideRequest) super.asFile(); } @Override protected void setRequestOptions(@NonNull RequestOptions toSet) { if (toSet instanceof com.bumptech.glide.test.GlideOptions) { super.setRequestOptions(toSet); } else { super.setRequestOptions(new com.bumptech.glide.test.GlideOptions().apply(toSet)); } } } ================================================ FILE: annotation/compiler/test/src/test/resources/MultipleAppGlideModuleTest/EmptyAppModule1.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public final class EmptyAppModule1 extends AppGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/MultipleAppGlideModuleTest/EmptyAppModule2.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public final class EmptyAppModule2 extends AppGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/MultipleEmptyLibraryGlideModuleTest/EmptyLibraryModule1.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public final class EmptyLibraryModule1 extends LibraryGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/MultipleEmptyLibraryGlideModuleTest/EmptyLibraryModule2.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public final class EmptyLibraryModule2 extends LibraryGlideModule {} ================================================ FILE: annotation/compiler/test/src/test/resources/MultipleEmptyLibraryGlideModuleTest/GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule1_com_bumptech_glide_test_EmptyLibraryModule2.java ================================================ package com.bumptech.glide.annotation.compiler; @Index( modules = { "com.bumptech.glide.test.EmptyLibraryModule1", "com.bumptech.glide.test.EmptyLibraryModule2" } ) public class GlideIndexer_GlideModule_com_bumptech_glide_test_EmptyLibraryModule1_com_bumptech_glide_test_EmptyLibraryModule2 { } ================================================ FILE: annotation/gradle.properties ================================================ POM_NAME=Glide Annotations POM_ARTIFACT_ID=annotations POM_PACKAGING=jar POM_DESCRIPTION=A set of annotations for configuring Glide. ================================================ FILE: annotation/ksp/build.gradle.kts ================================================ plugins { id("org.jetbrains.kotlin.jvm") id("com.google.devtools.ksp") } kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(11)) } } dependencies { implementation(libs.kotlinpoet) implementation(project(":annotation")) implementation(libs.ksp.api) implementation(libs.autoservice.annotations) ksp(libs.ksp.autoservice) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: annotation/ksp/gradle.properties ================================================ kotlin.code.style=official POM_NAME=Glide KSP Annotation Processor POM_ARTIFACT_ID=ksp POM_PACKAGING=jar POM_DESCRIPTION=Glide's KSP based annotation processor. Should be included in all Kotlin applications and libraries that use Glide's modules for configuration and do not require the more advanced features of the Java based compiler. ================================================ FILE: annotation/ksp/integrationtest/build.gradle.kts ================================================ /** * This package verifies that our ksp processor is able to successfully import * and include LibraryGlideModules compiled in other modules. ksp:test is a more * comprehensive set of unit tests for other scenarios for library tests. * *

Technically we could include these integration tests in ksp:test. However * doing so would cause the dependent library to pollute every individual test * because it's pulled in from the classpath. Using a separate module allows us * to keep unit tests that are not concerned with dependent library modules * separate. */ plugins { id("org.jetbrains.kotlin.android") id("com.android.library") } android { namespace = "com.bumptech.glide.annotation.ksp.integrationtest" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(11)) } } dependencies { implementation(libs.junit) testImplementation(project(":annotation:ksp:test")) testImplementation(project(":annotation:ksp")) testImplementation(project(":annotation")) testImplementation(project(":glide")) testImplementation(project(":integration:okhttp3")) testImplementation(libs.ksp.compiletesting) testImplementation(libs.truth) testImplementation(libs.kotlin.test) testImplementation(project(":annotation:ksp:test")) } ================================================ FILE: annotation/ksp/integrationtest/src/test/java/com/bumptech/glide/annotation/ksp/integrationtest/IntegrationLibraryGlideModuleTests.kt ================================================ package com.bumptech.glide.annotation.ksp.integrationtest import com.bumptech.glide.annotation.ksp.test.CommonSources import com.bumptech.glide.annotation.ksp.test.JavaSourceFile import com.bumptech.glide.annotation.ksp.test.KotlinSourceFile import com.bumptech.glide.annotation.ksp.test.PerSourceTypeTest import com.bumptech.glide.annotation.ksp.test.SourceType import com.bumptech.glide.annotation.ksp.test.hasSourceEqualTo import com.google.common.truth.Truth.assertThat import com.tschuchort.compiletesting.KotlinCompilation.ExitCode import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters @OptIn(ExperimentalCompilerApi::class) @RunWith(Parameterized::class) class IntegrationLibraryGlideModuleTests(override val sourceType: SourceType) : PerSourceTypeTest { companion object { @Parameters(name = "sourceType = {0}") @JvmStatic fun data() = SourceType.values() } @Test fun compile_withOnlyAppGlideModule_generatesGeneratedAppGlideModule_thatCallsDependencyLibraryGlideModules() { val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType(kotlinAppModule, javaAppModule) { assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithOnlyDependencyLibraryModules) } } @Test fun compile_withOnlyAppGlideModuleThroughBaseClass_generatesGeneratedAppGlideModule_thatCallsDependencyLibraryGlideModules() { val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule class BaseAppModule : AppGlideModule() @GlideModule class AppModule : BaseAppModule() """, ) val javaBaseAppModule = JavaSourceFile( "BaseAppModule.java", """ import com.bumptech.glide.module.AppGlideModule; public class BaseAppModule extends AppGlideModule { public BaseAppModule() {} } """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; @GlideModule public class AppModule extends BaseAppModule { public AppModule() {} } """, ) compileCurrentSourceType(kotlinAppModule, javaBaseAppModule, javaAppModule) { assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithOnlyDependencyLibraryModules) } } @Test fun compile_withValidLibraryGlideModule_andAppGlideModule_generatesGeneratedAppGlideModule_thatCallsAllLibraryAndDependencyAndAppGlideModules() { val kotlinLibraryModule = KotlinSourceFile( "LibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """, ) val javaLibraryModule = JavaSourceFile( "LibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class LibraryModule extends LibraryGlideModule {} """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule, javaAppModule, javaLibraryModule, ) { assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithLibraryModuleAndDependencyLibraryModules) } } @Test fun compile_withValidLibraryGlideModule_andAppGlideModule_ThroughBaseClass_generatesGeneratedAppGlideModule_thatCallsAllLibraryAndDependencyAndAppGlideModules() { val kotlinLibraryModule = KotlinSourceFile( "LibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule class BaseLibraryModule : LibraryGlideModule() @GlideModule class LibraryModule : BaseLibraryModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule class BaseAppModule : AppGlideModule() @GlideModule class AppModule : BaseAppModule() """, ) val javaBaseLibraryModule = JavaSourceFile( "BaseLibraryModule.java", """ import com.bumptech.glide.module.LibraryGlideModule; public class BaseLibraryModule extends LibraryGlideModule {} """, ) val javaLibraryModule = JavaSourceFile( "LibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; @GlideModule public class LibraryModule extends BaseLibraryModule {} """, ) val javaBaseAppModule = JavaSourceFile( "BaseAppModule.java", """ import com.bumptech.glide.module.AppGlideModule; public class BaseAppModule extends AppGlideModule { public BaseAppModule() {} } """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; @GlideModule public class AppModule extends BaseAppModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule, javaBaseAppModule, javaAppModule, javaBaseLibraryModule, javaLibraryModule, ) { assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithLibraryModuleAndDependencyLibraryModules) } } @Test fun compile_withDependencyModuleInExcludes_generatesGeneratedAppGlideModule_thatDoesNotCallDependencyLibraryGlideModules() { val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.Excludes import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule @Excludes(OkHttpLibraryGlideModule::class) @GlideModule class AppModule : AppGlideModule() """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule; @Excludes(OkHttpLibraryGlideModule.class) @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType(kotlinAppModule, javaAppModule) { assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(CommonSources.simpleAppGlideModule) } } @Test fun compile_withLibraryModuleInExcludes_producesGeneratedAppGlideModuleThatDoesNotCallExcludedLibraryModule() { val kotlinExcludedLibraryModule = KotlinSourceFile( "ExcludedLibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class ExcludedLibraryModule : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.Excludes import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule @Excludes(ExcludedLibraryModule::class) class AppModule : AppGlideModule() """, ) val javaExcludedLibraryModule = JavaSourceFile( "ExcludedLibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class ExcludedLibraryModule extends LibraryGlideModule {} """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule @Excludes(ExcludedLibraryModule.class) public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinExcludedLibraryModule, javaAppModule, javaExcludedLibraryModule, ) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithOnlyDependencyLibraryModules) assertThat(it.exitCode).isEqualTo(ExitCode.OK) } } } // generated code always includes public and Unit @Suppress("RedundantVisibilityModifier", "RedundantUnitReturnType") @Language("kotlin") const val appGlideModuleWithLibraryModuleAndDependencyLibraryModules = """ package com.bumptech.glide import AppModule import LibraryModule import android.content.Context import com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule import kotlin.Boolean import kotlin.Suppress import kotlin.Unit internal class GeneratedAppGlideModuleImpl( @Suppress("UNUSED_PARAMETER") context: Context, ) : GeneratedAppGlideModule() { private val appGlideModule: AppModule init { appGlideModule = AppModule() } public override fun registerComponents( context: Context, glide: Glide, registry: Registry, ): Unit { OkHttpLibraryGlideModule().registerComponents(context, glide, registry) LibraryModule().registerComponents(context, glide, registry) appGlideModule.registerComponents(context, glide, registry) } public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { appGlideModule.applyOptions(context, builder) } public override fun isManifestParsingEnabled(): Boolean = false } """ // generated code always includes public and Unit @Suppress("RedundantVisibilityModifier", "RedundantUnitReturnType") @Language("kotlin") const val appGlideModuleWithOnlyDependencyLibraryModules = """ package com.bumptech.glide import AppModule import android.content.Context import com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule import kotlin.Boolean import kotlin.Suppress import kotlin.Unit internal class GeneratedAppGlideModuleImpl( @Suppress("UNUSED_PARAMETER") context: Context, ) : GeneratedAppGlideModule() { private val appGlideModule: AppModule init { appGlideModule = AppModule() } public override fun registerComponents( context: Context, glide: Glide, registry: Registry, ): Unit { OkHttpLibraryGlideModule().registerComponents(context, glide, registry) appGlideModule.registerComponents(context, glide, registry) } public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { appGlideModule.applyOptions(context, builder) } public override fun isManifestParsingEnabled(): Boolean = false } """ ================================================ FILE: annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/AppGlideModules.kt ================================================ package com.bumptech.glide.annotation.ksp import com.bumptech.glide.annotation.Excludes import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.getConstructors import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSNode import com.google.devtools.ksp.symbol.KSType import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.TypeSpec import kotlin.reflect.KClass // This class is visible only for testing // TODO(b/174783094): Add @VisibleForTesting when internal is supported. object AppGlideModuleConstants { // This variable is visible only for testing // TODO(b/174783094): Add @VisibleForTesting when internal is supported. const val INVALID_MODULE_MESSAGE = "Your AppGlideModule must have at least one constructor that has either no parameters or " + "accepts only a Context." // This variable is visible only for testing // TODO(b/174783094): Add @VisibleForTesting when internal is supported. const val INVALID_EXCLUDES_ANNOTATION_MESSAGE = """ @Excludes on %s is invalid. The value argument of your @Excludes annotation must be set to either a single LibraryGlideModule class or a non-empty list of LibraryGlideModule classes. Remove the annotation if you do not wish to exclude any LibraryGlideModules. Include each LibraryGlideModule you do wish to exclude exactly once. Do not put types other than LibraryGlideModules in the argument list""" private const val CONTEXT_NAME = "Context" private const val CONTEXT_PACKAGE = "android.content" internal const val GLIDE_PACKAGE_NAME = "com.bumptech.glide" internal const val CONTEXT_QUALIFIED_NAME = "$CONTEXT_PACKAGE.$CONTEXT_NAME" internal const val GENERATED_ROOT_MODULE_PACKAGE_NAME = GLIDE_PACKAGE_NAME internal val CONTEXT_CLASS_NAME = ClassName(CONTEXT_PACKAGE, CONTEXT_NAME) } internal data class AppGlideModuleData( val name: ClassName, val constructor: Constructor, val allowedLibraryGlideModuleNames: List, val sources: List, ) { internal data class Constructor(val hasContext: Boolean) } /** * Given a [com.bumptech.glide.module.AppGlideModule] class declaration provided by the developer, * validate the class and produce a fully parsed [AppGlideModuleData] that allows us to generate a * valid [com.bumptech.glide.GeneratedAppGlideModule] implementation without further introspection. */ internal class AppGlideModuleParser( private val environment: SymbolProcessorEnvironment, private val resolver: Resolver, private val appGlideModuleClass: KSClassDeclaration, ) { fun parseAppGlideModule(): AppGlideModuleData { val constructor = parseAppGlideModuleConstructorOrThrow() val name = ClassName.bestGuess(appGlideModuleClass.qualifiedName!!.asString()) val (indexFiles, allLibraryModuleNames) = getIndexesAndLibraryGlideModuleNames() val excludedGlideModuleClassNames = getExcludedGlideModuleClassNames() val filteredGlideModuleClassNames = allLibraryModuleNames.filterNot { excludedGlideModuleClassNames.contains(it) } return AppGlideModuleData( name = name, constructor = constructor, allowedLibraryGlideModuleNames = filteredGlideModuleClassNames, sources = indexFiles, ) } private fun getExcludedGlideModuleClassNames(): Set { val excludesAnnotation = appGlideModuleClass.atMostOneExcludesAnnotation() ?: return emptySet() environment.logger.logging( "Found excludes annotation arguments: ${excludesAnnotation.arguments}" ) return parseExcludesAnnotationArgumentsOrNull(excludesAnnotation) ?: throw InvalidGlideSourceException( AppGlideModuleConstants.INVALID_EXCLUDES_ANNOTATION_MESSAGE.format( appGlideModuleClass.qualifiedName?.asString() ) ) } /** * Given a list of arguments from an [com.bumptech.glide.annotation.Excludes] annotation, parses * and returns a list of qualified names of the excluded * [com.bumptech.glide.module.LibraryGlideModule] implementations, or returns null if the * arguments are invalid. * * Ideally we'd throw more specific exceptions based on the type of failure. However, there are * a bunch of individual failure types and they differ depending on whether the source was * written in Java or Kotlin. Rather than trying to describe every failure in detail, we'll just * return null and allow callers to describe the correct behavior. */ private fun parseExcludesAnnotationArgumentsOrNull( excludesAnnotation: KSAnnotation ): Set? { val valueArguments: List? = excludesAnnotation.valueArgumentList() if (valueArguments == null || valueArguments.isEmpty()) { return null } if (valueArguments.any { !it.extendsLibraryGlideModule() }) { return null } val libraryGlideModuleNames = valueArguments.mapNotNull { it.declaration.qualifiedName?.asString() } if (libraryGlideModuleNames.size != valueArguments.size) { return null } val uniqueLibraryGlideModuleNames = libraryGlideModuleNames.toSet() if (uniqueLibraryGlideModuleNames.size != valueArguments.size) { return null } return uniqueLibraryGlideModuleNames } private fun KSType.extendsLibraryGlideModule(): Boolean = ModuleParser.extractGlideModules(listOf(declaration)).libraryModules.size == 1 /** * Parses the `value` argument as a list of the given type, or returns `null` if the annotation * has multiple arguments or `value` has any entries that are not of the expected type `T`. * * `value` is the name of the default annotation parameter allowed by syntax like * `@Excludes(argument)` or `@Excludes(argument1, argument2)` or `@Excludes({argument1, * argument2})`, depending on the source type (Kotlin or Java). This method requires that the * annotation has exactly one `value` argument of a given type and standardizes the differences * KSP produces between Kotlin and Java source. * * To make this function more general purpose, we should assert that the values are of type T * rather just returning null. For our current single use case, returning null matches the use * case for the caller better than throwing. */ private inline fun KSAnnotation.valueArgumentList(): List? { // Require that the annotation has a single value argument that points either to a single // thing // or a list of things (A or [A, B, C]). First validate that there's exactly one parameter // and // that it has the expected name. // e.g. @Excludes(value = (A or [A, B, C])) -> (A or [A, B, C]) val valueParameterValue: Any? = arguments.singleOrNull().takeIf { it?.name?.asString() == "value" }?.value // Next unify the types by verifying that it either has a single value of T, or a List of // T and converting both to List // (A or [A, B, C]) -> ([A] or [A, B, C]) with the correct types return when (valueParameterValue) { is List<*> -> valueParameterValue.asListGivenTypeOfOrNull() is T -> listOf(valueParameterValue) else -> null } } private inline fun List<*>.asListGivenTypeOfOrNull(): List? = filterIsInstance(T::class.java).takeIf { it.size == size } private fun parseAppGlideModuleConstructorOrThrow(): AppGlideModuleData.Constructor { val hasEmptyConstructors = appGlideModuleClass.getConstructors().any { it.parameters.isEmpty() } val hasContextParamOnlyConstructor = appGlideModuleClass.getConstructors().any { it.hasSingleContextParameter() } if (!hasEmptyConstructors && !hasContextParamOnlyConstructor) { throw InvalidGlideSourceException(AppGlideModuleConstants.INVALID_MODULE_MESSAGE) } return AppGlideModuleData.Constructor(hasContextParamOnlyConstructor) } private fun KSFunctionDeclaration.hasSingleContextParameter() = parameters.size == 1 && AppGlideModuleConstants.CONTEXT_QUALIFIED_NAME == parameters.single().type.resolve().declaration.qualifiedName?.asString() private data class IndexFilesAndLibraryModuleNames( val indexFiles: List, val libraryModuleNames: List, ) @OptIn(KspExperimental::class) private fun getIndexesAndLibraryGlideModuleNames(): IndexFilesAndLibraryModuleNames { val allIndexFiles: MutableList = mutableListOf() val allLibraryGlideModuleNames: MutableList = mutableListOf() val allIndexesAndLibraryModules = getAllLibraryNamesFromJavaIndexes() + getAllLibraryNamesFromKspIndexes() for ((index, libraryGlideModuleNames) in allIndexesAndLibraryModules) { allIndexFiles.add(index) allLibraryGlideModuleNames.addAll(libraryGlideModuleNames) } return IndexFilesAndLibraryModuleNames(allIndexFiles, allLibraryGlideModuleNames) } internal data class IndexAndLibraryModuleNames( val index: KSDeclaration, val libraryModuleNames: List, ) private fun getAllLibraryNamesFromKspIndexes(): List = getAllLibraryNamesFromIndexes(GlideSymbolProcessorConstants.PACKAGE_NAME) { index -> extractGlideModulesFromKspIndexAnnotation(index) } private fun getAllLibraryNamesFromJavaIndexes(): List = getAllLibraryNamesFromIndexes(GlideSymbolProcessorConstants.JAVA_ANNOTATION_PACKAGE_NAME) { index -> extractGlideModulesFromJavaIndexAnnotation(index) } @OptIn(KspExperimental::class) private fun getAllLibraryNamesFromIndexes( packageName: String, extractLibraryModuleNamesFromIndex: (KSDeclaration) -> List, ) = buildList { resolver.getDeclarationsFromPackage(packageName).forEach { index: KSDeclaration -> val libraryGlideModuleNames = extractLibraryModuleNamesFromIndex(index) if (libraryGlideModuleNames.isNotEmpty()) { environment.logger.info( "Found index annotation: $index with modules: $libraryGlideModuleNames" ) add(IndexAndLibraryModuleNames(index, libraryGlideModuleNames)) } } } private fun extractGlideModulesFromJavaIndexAnnotation(index: KSDeclaration): List { val indexAnnotation: KSAnnotation = index.atMostOneJavaIndexAnnotation() ?: return emptyList() return indexAnnotation.getModuleArgumentValues().toList() } private fun extractGlideModulesFromKspIndexAnnotation(index: KSDeclaration): List { val indexAnnotation: KSAnnotation = index.atMostOneKspIndexAnnotation() ?: return emptyList() return indexAnnotation.getModuleArgumentValues().toList() } private fun KSAnnotation.getModuleArgumentValues(): List { val result = arguments .find { it.name?.getShortName().equals(IndexGenerator.INDEX_MODULES_NAME) } ?.value if (result is List<*> && result.all { it is String }) { @Suppress("UNCHECKED_CAST") return result as List } throw InvalidGlideSourceException("Found an invalid internal Glide index: $this") } private fun KSDeclaration.atMostOneJavaIndexAnnotation() = atMostOneAnnotation("com.bumptech.glide.annotation.compiler.Index") private fun KSDeclaration.atMostOneKspIndexAnnotation() = atMostOneAnnotation(Index::class) private fun KSDeclaration.atMostOneExcludesAnnotation() = atMostOneAnnotation(Excludes::class) private fun KSDeclaration.atMostOneAnnotation( annotation: KClass ): KSAnnotation? = atMostOneAnnotation(annotation.qualifiedName) private fun KSDeclaration.atMostOneAnnotation(annotationQualifiedName: String?): KSAnnotation? { val matchingAnnotations: List = annotations .filter { annotationQualifiedName?.equals( it.annotationType.resolve().declaration.qualifiedName?.asString() ) ?: false } .toList() if (matchingAnnotations.size > 1) { throw InvalidGlideSourceException( """Expected 0 or 1 $annotationQualifiedName annotations on $qualifiedName, but found: ${matchingAnnotations.size}""" ) } return matchingAnnotations.singleOrNull() } } /** * Given valid [AppGlideModuleData], writes a Kotlin implementation of * [com.bumptech.glide.GeneratedAppGlideModule]. * * This class should obtain all of its data from [AppGlideModuleData] and should not interact with * any ksp classes. In the long run, the restriction may allow us to share code between the Java and * Kotlin processors. */ internal class AppGlideModuleGenerator(private val appGlideModuleData: AppGlideModuleData) { fun generateAppGlideModule(): FileSpec { val generatedAppGlideModuleClass = generateAppGlideModuleClass(appGlideModuleData) return FileSpec.builder( AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "GeneratedAppGlideModuleImpl", ) .addType(generatedAppGlideModuleClass) .build() } private fun generateAppGlideModuleClass(data: AppGlideModuleData): TypeSpec { return TypeSpec.classBuilder("GeneratedAppGlideModuleImpl") .superclass( ClassName( AppGlideModuleConstants.GENERATED_ROOT_MODULE_PACKAGE_NAME, "GeneratedAppGlideModule", ) ) .addModifiers(KModifier.INTERNAL) .addProperty("appGlideModule", data.name, KModifier.PRIVATE) .primaryConstructor(generateConstructor(data)) .addFunction(generateRegisterComponents(data.allowedLibraryGlideModuleNames)) .addFunction(generateApplyOptions()) .addFunction(generateManifestParsingDisabled()) .build() } private fun generateConstructor(data: AppGlideModuleData): FunSpec { val contextParameterBuilder = ParameterSpec.builder("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) if (!data.constructor.hasContext) { contextParameterBuilder.addAnnotation( AnnotationSpec.builder(ClassName("kotlin", "Suppress")) .addMember("%S", "UNUSED_PARAMETER") .build() ) } return FunSpec.constructorBuilder() .addParameter(contextParameterBuilder.build()) .addStatement( "appGlideModule = %T(${if (data.constructor.hasContext) "context" else ""})", data.name, ) .build() // TODO(judds): Log the discovered modules here. } private fun generateRegisterComponents(allowedGlideModuleNames: List) = FunSpec.builder("registerComponents") .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) .addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) .addParameter("glide", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Glide")) .addParameter( "registry", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Registry"), ) .apply { allowedGlideModuleNames.forEach { addStatement( "%T().registerComponents(context, glide, registry)", ClassName.bestGuess(it), ) } } .addStatement("appGlideModule.registerComponents(context, glide, registry)") .build() private fun generateApplyOptions() = FunSpec.builder("applyOptions") .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) .addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME) .addParameter( "builder", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "GlideBuilder"), ) .addStatement("appGlideModule.applyOptions(context, builder)") .build() private fun generateManifestParsingDisabled() = FunSpec.builder("isManifestParsingEnabled") .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) .returns(Boolean::class) .addStatement("return false") .build() } ================================================ FILE: annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessor.kt ================================================ package com.bumptech.glide.annotation.ksp import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFile import com.google.devtools.ksp.validate import com.squareup.kotlinpoet.FileSpec /** * Glide's KSP annotation processor. * * This class recognizes and parses [com.bumptech.glide.module.AppGlideModule]s and * [com.bumptech.glide.module.LibraryGlideModule]s that are annotated with * [com.bumptech.glide.annotation.GlideModule]. * * `LibraryGlideModule`s are merged into indexes, or classes generated in Glide's package. When a * `AppGlideModule` is found, we then generate Glide's configuration so that it calls the * `AppGlideModule` and any included `LibraryGlideModules`. Using indexes allows us to process * `LibraryGlideModules` in multiple rounds and/or libraries. */ class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor { private var isAppGlideModuleGenerated = false override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation("com.bumptech.glide.annotation.GlideModule") val (validSymbols, invalidSymbols) = symbols.partition { it.validate() }.toList() return try { processChecked(resolver, symbols, validSymbols, invalidSymbols) } catch (e: InvalidGlideSourceException) { environment.logger.error(e.userMessage) invalidSymbols } } private fun processChecked( resolver: Resolver, symbols: Sequence, validSymbols: List, invalidSymbols: List, ): List { environment.logger.logging("Found symbols, valid: $validSymbols, invalid: $invalidSymbols") val (appGlideModules, libraryGlideModules) = ModuleParser.extractGlideModules(validSymbols) if (libraryGlideModules.size + appGlideModules.size != validSymbols.count()) { val invalidModules = symbols .filter { !libraryGlideModules.contains(it) && !appGlideModules.contains(it) } .map { it.location.toString() } .toList() throw InvalidGlideSourceException( GlideSymbolProcessorConstants.INVALID_ANNOTATED_CLASS.format(invalidModules) ) } if (appGlideModules.size > 1) { throw InvalidGlideSourceException( GlideSymbolProcessorConstants.SINGLE_APP_MODULE_ERROR.format(appGlideModules) ) } environment.logger.logging( "Found AppGlideModules: $appGlideModules, LibraryGlideModules: $libraryGlideModules" ) if (libraryGlideModules.isNotEmpty()) { if (isAppGlideModuleGenerated) { throw InvalidGlideSourceException( """Found $libraryGlideModules LibraryGlideModules after processing the AppGlideModule. If you generated these LibraryGlideModules via another annotation processing, either don't or also generate the AppGlideModule and do so in the same round as the LibraryGlideModules or in a subsequent round""" ) } parseLibraryModulesAndWriteIndex(libraryGlideModules) return invalidSymbols + appGlideModules } if (appGlideModules.isNotEmpty()) { parseAppGlideModuleAndIndexesAndWriteGeneratedAppGlideModule( resolver, appGlideModules.single(), ) } return invalidSymbols } private fun parseAppGlideModuleAndIndexesAndWriteGeneratedAppGlideModule( resolver: Resolver, appGlideModule: KSClassDeclaration, ) { val appGlideModuleData = AppGlideModuleParser(environment, resolver, appGlideModule).parseAppGlideModule() val appGlideModuleGenerator = AppGlideModuleGenerator(appGlideModuleData) val appGlideModuleFileSpec: FileSpec = appGlideModuleGenerator.generateAppGlideModule() val sources = appGlideModuleData.sources.mapNotNull { it.containingFile }.toMutableList() if (appGlideModule.containingFile != null) { sources.add(appGlideModule.containingFile!!) } writeFile(appGlideModuleFileSpec, sources) } private fun parseLibraryModulesAndWriteIndex( libraryGlideModuleClassDeclarations: List ) { val libraryGlideModulesParser = LibraryGlideModulesParser(environment, libraryGlideModuleClassDeclarations) val uniqueLibraryGlideModules = libraryGlideModulesParser.parseUnique() val index: FileSpec = IndexGenerator.generate(uniqueLibraryGlideModules.map { it.name }) writeFile(index, uniqueLibraryGlideModules.mapNotNull { it.containingFile }) } private fun writeFile(file: FileSpec, sources: List) { environment.codeGenerator .createNewFile( Dependencies(aggregating = false, sources = sources.toTypedArray()), file.packageName, file.name, ) .writer() .use { file.writeTo(it) } environment.logger.logging("Wrote file: $file") } } // This class is visible only for testing // TODO(b/174783094): Add @VisibleForTesting when internal is supported. object GlideSymbolProcessorConstants { // This variable is visible only for testing // TODO(b/174783094): Add @VisibleForTesting when internal is supported. val PACKAGE_NAME: String = GlideSymbolProcessor::class.java.`package`.name val JAVA_ANNOTATION_PACKAGE_NAME: String = "com.bumptech.glide.annotation.compiler" const val SINGLE_APP_MODULE_ERROR = "You can have at most one AppGlideModule, but found: %s" const val DUPLICATE_LIBRARY_MODULE_ERROR = "LibraryGlideModules %s are included more than once, keeping only one!" const val INVALID_ANNOTATED_CLASS = "@GlideModule annotated classes must implement AppGlideModule or LibraryGlideModule: %s" } internal class InvalidGlideSourceException(val userMessage: String) : Exception(userMessage) ================================================ FILE: annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/GlideSymbolProcessorProvider.kt ================================================ package com.bumptech.glide.annotation.ksp import com.google.auto.service.AutoService import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider @AutoService(SymbolProcessorProvider::class) class GlideSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return GlideSymbolProcessor(environment) } } ================================================ FILE: annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/LibraryGlideModules.kt ================================================ package com.bumptech.glide.annotation.ksp import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.annotation.ksp.LibraryGlideModuleData.LibraryModuleName import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFile import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.DelicateKotlinPoetApi import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec import java.util.UUID internal data class LibraryGlideModuleData( val name: LibraryModuleName, val containingFile: KSFile?, ) { data class LibraryModuleName(val qualifiedName: String) } internal class LibraryGlideModulesParser( private val environment: SymbolProcessorEnvironment, private val libraryGlideModules: List, ) { init { require(libraryGlideModules.isNotEmpty()) } fun parseUnique(): List { val allLibraryGlideModules = libraryGlideModules .map { LibraryGlideModuleData( LibraryModuleName(it.qualifiedName!!.asString()), it.containingFile, ) } .toList() val uniqueLibraryGlideModules = allLibraryGlideModules.associateBy { it.name }.values.toList() if (uniqueLibraryGlideModules.size != libraryGlideModules.size) { // Find the set of modules that have been included more than once by mapping the // qualified // name of the module to a count of the number of times it's been seen. Duplicates are // then // any keys that have a value > 1. val duplicateModules: List = allLibraryGlideModules .groupingBy { it.name.qualifiedName } .eachCount() .filter { it.value > 1 } .keys .toList() environment.logger.warn( GlideSymbolProcessorConstants.DUPLICATE_LIBRARY_MODULE_ERROR.format( duplicateModules ) ) } return uniqueLibraryGlideModules } } /** * Generates an empty class with an annotation containing the class names of one or more * LibraryGlideModules and/or one or more GlideExtensions. * * We use a separate class so that LibraryGlideModules and GlideExtensions written in libraries can * be bundled into an AAR and later retrieved by the annotation processor when it processes the * AppGlideModule in an application. * * The output file generated by this class with a single LibraryGlideModule looks like this: * ``` * @com.bumptech.glide.annotation.ksp.Index( * ["com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule"] * ) * class Indexer_GlideModule_com_bumptech_glide_integration_okhttp3_OkHttpLibraryGlideModule * ``` * * This class is not a public API and used only internally by the processor. */ internal object IndexGenerator { private const val INDEXER_NAME_PREFIX = "GlideIndexer_" private const val MAXIMUM_FILE_NAME_LENGTH = 255 // The name of the parameter in the Index annotation that points to the list of modules internal const val INDEX_MODULES_NAME = "modules" @OptIn(DelicateKotlinPoetApi::class) // For AnnotationSpec.builder fun generate(libraryModuleNames: List): FileSpec { val libraryModuleQualifiedNames: List = libraryModuleNames.map { it.qualifiedName } val indexAnnotation: AnnotationSpec = AnnotationSpec.builder(Index::class.java) .addRepeatedMember(INDEX_MODULES_NAME, libraryModuleQualifiedNames) .build() val indexName = generateUniqueName(libraryModuleQualifiedNames) return FileSpec.builder(GlideSymbolProcessorConstants.PACKAGE_NAME, indexName) .addType(TypeSpec.classBuilder(indexName).addAnnotation(indexAnnotation).build()) .build() } private fun generateUniqueName(libraryModuleQualifiedNames: List): String { val glideModuleBasedName = generateNameFromLibraryModules(libraryModuleQualifiedNames) // If the indexer name has too many packages/modules, it can exceed the file name length // allowed by the file system, which can break compilation. To avoid that, fall back to a // deterministic UUID. return if (glideModuleBasedName.exceedsFileSystemMaxNameLength()) { generateShortUUIDBasedName(glideModuleBasedName) } else { glideModuleBasedName } } private fun String.exceedsFileSystemMaxNameLength() = length >= MAXIMUM_FILE_NAME_LENGTH - INDEXER_NAME_PREFIX.length private fun generateShortUUIDBasedName(glideModuleBasedName: String) = INDEXER_NAME_PREFIX + UUID.nameUUIDFromBytes(glideModuleBasedName.toByteArray()).toString().replace("-", "_") private fun generateNameFromLibraryModules(libraryModuleQualifiedNames: List): String { return libraryModuleQualifiedNames.joinToString( prefix = INDEXER_NAME_PREFIX + GlideModule::class.java.simpleName + "_", separator = "_", ) { it.replace(".", "_") } } private fun AnnotationSpec.Builder.addRepeatedMember( name: String, repeatedMember: List, ) = addMember( "$name = [\n" + "%S,\n".repeat(repeatedMember.size) + "]", *repeatedMember.toTypedArray(), ) } ================================================ FILE: annotation/ksp/src/main/kotlin/com/bumptech/glide/annotation/ksp/ModuleParser.kt ================================================ package com.bumptech.glide.annotation.ksp import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSNode object ModuleParser { internal data class GlideModules( val appModules: List, val libraryModules: List, ) internal fun extractGlideModules(annotatedModules: List): GlideModules { val appAndLibraryModuleNames = listOf(APP_MODULE_QUALIFIED_NAME, LIBRARY_MODULE_QUALIFIED_NAME) val modulesBySuperType: Map> = annotatedModules.filterIsInstance().groupBy { classDeclaration -> appAndLibraryModuleNames.firstOrNull { classDeclaration.hasSuperType(it) } } val (appModules, libraryModules) = appAndLibraryModuleNames.map { modulesBySuperType[it] ?: emptyList() } return GlideModules(appModules, libraryModules) } private fun KSClassDeclaration.hasSuperType(superTypeQualifiedName: String): Boolean { val superDeclarations = superTypes.map { superType -> superType.resolve().declaration } val hasInDirectParent = superDeclarations.map { it.qualifiedName!!.asString() }.contains(superTypeQualifiedName) return if (hasInDirectParent) { true } else { superDeclarations.filterIsInstance(KSClassDeclaration::class.java).any { it.hasSuperType(superTypeQualifiedName) } } } private const val APP_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.AppGlideModule" private const val LIBRARY_MODULE_QUALIFIED_NAME = "com.bumptech.glide.module.LibraryGlideModule" } ================================================ FILE: annotation/ksp/test/build.gradle.kts ================================================ plugins { id("org.jetbrains.kotlin.android") id("com.android.library") } android { namespace = "com.bumptech.glide.annotation.ksp.test" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(11)) } } dependencies { implementation(libs.junit) implementation(project(":annotation:ksp")) implementation(libs.ksp.compiletesting) implementation(libs.truth) testImplementation(project(":annotation:ksp")) testImplementation(project(":annotation")) testImplementation(project(":glide")) testImplementation(libs.kotlin.test) } ================================================ FILE: annotation/ksp/test/src/main/kotlin/com/bumptech/glide/annotation/ksp/test/SourceTestHelpers.kt ================================================ package com.bumptech.glide.annotation.ksp.test import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorProvider import com.google.common.truth.StringSubject import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.kspSourcesDir import com.tschuchort.compiletesting.symbolProcessorProviders import java.io.File import java.io.FileNotFoundException import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi @OptIn(ExperimentalCompilerApi::class) class CompilationResult( private val compilation: KotlinCompilation, result: KotlinCompilation.Result, ) { val exitCode = result.exitCode val messages = result.messages fun generatedAppGlideModuleContents() = readFile(findAppGlideModule()) fun allGeneratedFiles(): List { val allFiles = mutableListOf() val parentDir = generatedFilesParentDir() if (parentDir != null) { findAllFilesRecursive(parentDir, allFiles) } return allFiles } private fun findAllFilesRecursive(parent: File, allFiles: MutableList) { if (parent.isFile) { allFiles.add(parent) return } parent.listFiles()?.map { findAllFilesRecursive(it, allFiles) } } private fun generatedFilesParentDir(): File? { var currentDir: File? = compilation.kspSourcesDir listOf("kotlin", "com", "bumptech", "glide").forEach { directoryName -> currentDir = currentDir?.listFiles()?.find { it.name.equals(directoryName) } } return currentDir } private fun readFile(file: File) = file.readLines().joinToString("\n") private fun findAppGlideModule(): File { return generatedFilesParentDir()?.listFiles()?.find { it.name.equals("GeneratedAppGlideModuleImpl.kt") } ?: throw FileNotFoundException( "GeneratedAppGlideModuleImpl.kt was not generated or not generated in the expected" + "location" ) } } enum class SourceType { KOTLIN, JAVA } sealed interface TypedSourceFile { fun sourceFile(): SourceFile fun sourceType(): SourceType } class GeneratedSourceFile( private val file: File, private val currentSourceType: SourceType, ) : TypedSourceFile { override fun sourceFile(): SourceFile = SourceFile.fromPath(file) // Hack alert: We use this class only for generated output of some previous compilation. We rely // on the type in that previous compilation to select the proper source. The output however is // always Kotlin, regardless of source. But we always want to include whatever the generated // output is in the next step. That means we need our sourceType here to match the // currentSourceType in the test. override fun sourceType(): SourceType = currentSourceType } class KotlinSourceFile( val name: String, @Language("kotlin") val content: String, ) : TypedSourceFile { override fun sourceFile() = SourceFile.kotlin(name, content) override fun sourceType() = SourceType.KOTLIN } class JavaSourceFile( val name: String, @Language("java") val content: String, ) : TypedSourceFile { override fun sourceFile() = SourceFile.java(name, content) override fun sourceType() = SourceType.JAVA } interface PerSourceTypeTest { val sourceType: SourceType fun compileCurrentSourceType( vararg sourceFiles: TypedSourceFile, test: (input: CompilationResult) -> Unit = {}, ): CompilationResult { val result = compile(sourceFiles.filter { it.sourceType() == sourceType }.map { it.sourceFile() }.toList()) test(result) return result } } @OptIn(ExperimentalCompilerApi::class) internal fun compile(sourceFiles: List): CompilationResult { require(sourceFiles.isNotEmpty()) val compilation = KotlinCompilation().apply { sources = sourceFiles symbolProcessorProviders = listOf(GlideSymbolProcessorProvider()) inheritClassPath = true } val result = compilation.compile() return CompilationResult(compilation, result) } fun StringSubject.hasSourceEqualTo(sourceContents: String) = isEqualTo(sourceContents.trimIndent()) object CommonSources { // generated code always includes public and Unit @Suppress("RedundantVisibilityModifier", "RedundantUnitReturnType") @Language("kotlin") const val simpleAppGlideModule = """ package com.bumptech.glide import AppModule import android.content.Context import kotlin.Boolean import kotlin.Suppress import kotlin.Unit internal class GeneratedAppGlideModuleImpl( @Suppress("UNUSED_PARAMETER") context: Context, ) : GeneratedAppGlideModule() { private val appGlideModule: AppModule init { appGlideModule = AppModule() } public override fun registerComponents( context: Context, glide: Glide, registry: Registry, ): Unit { appGlideModule.registerComponents(context, glide, registry) } public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { appGlideModule.applyOptions(context, builder) } public override fun isManifestParsingEnabled(): Boolean = false } """ } ================================================ FILE: annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/LibraryGlideModuleTests.kt ================================================ package com.bumptech.glide.annotation.ksp.test import com.bumptech.glide.annotation.ksp.AppGlideModuleConstants import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorConstants import com.google.common.truth.Truth.assertThat import com.tschuchort.compiletesting.KotlinCompilation.ExitCode import java.io.FileNotFoundException import kotlin.test.assertFailsWith import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters @RunWith(Parameterized::class) @OptIn(ExperimentalCompilerApi::class) class LibraryGlideModuleTests(override val sourceType: SourceType) : PerSourceTypeTest { companion object { @Parameters(name = "sourceType = {0}") @JvmStatic fun data() = SourceType.values() } @Test fun compile_withAnnotatedAndValidLibraryGlideModule_succeeds_butDoesNotGenerateGeneratedAppGlideModule() { val kotlinModule = KotlinSourceFile( "Module.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class Module : LibraryGlideModule() """, ) val javaModule = JavaSourceFile( "Module.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class Module extends LibraryGlideModule {} """, ) compileCurrentSourceType(kotlinModule, javaModule) { assertThat(it.messages).doesNotContainMatch("[we]: \\[ksp] .*") assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertFailsWith { it.generatedAppGlideModuleContents() } } } @Test fun compile_withValidLibraryGlideModule_andAppGlideModule_generatesGeneratedAppGlideModule_andCallsBothLibraryAndAppGlideModules() { val kotlinLibraryModule = KotlinSourceFile( "LibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """, ) val javaLibraryModule = JavaSourceFile( "LibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class LibraryModule extends LibraryGlideModule {} """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule, javaAppModule, javaLibraryModule, ) { assertThat(it.messages).doesNotContainMatch("[we]: \\[ksp] .*") assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithLibraryModule) } } @Test fun compile_withValidLibraryGlideModule_andAppGlideModule_ThroughBaseClass_generatesGeneratedAppGlideModule_andCallsBothLibraryAndAppGlideModules() { val kotlinLibraryModule = KotlinSourceFile( "LibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule class BaseLibraryModule : LibraryGlideModule() @GlideModule class LibraryModule : BaseLibraryModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule class BaseAppModule : AppGlideModule() @GlideModule class AppModule : BaseAppModule() """, ) val javaBaseLibraryModule = JavaSourceFile( "BaseLibraryModule.java", """ import com.bumptech.glide.module.LibraryGlideModule; public class BaseLibraryModule extends LibraryGlideModule {} """, ) val javaLibraryModule = JavaSourceFile( "LibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; @GlideModule public class LibraryModule extends BaseLibraryModule {} """, ) val javaBaseAppModule = JavaSourceFile( "BaseAppModule.java", """ import com.bumptech.glide.module.AppGlideModule; public class BaseAppModule extends AppGlideModule { public BaseAppModule() {} } """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; @GlideModule public class AppModule extends BaseAppModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule, javaBaseAppModule, javaAppModule, javaBaseLibraryModule, javaLibraryModule, ) { assertThat(it.messages).doesNotContainMatch("[we]: \\[ksp] .*") assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithLibraryModule) } } @Test fun compile_withMultipleLibraryGlideModules_andAppGlideModule_callsAllLibraryGlideModulesFromGeneratedAppGlideModule() { val kotlinLibraryModule1 = KotlinSourceFile( "LibraryModule1.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule1 : LibraryGlideModule() """, ) val kotlinLibraryModule2 = KotlinSourceFile( "LibraryModule2.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule2 : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """, ) val javaLibraryModule1 = JavaSourceFile( "LibraryModule1.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class LibraryModule1 extends LibraryGlideModule {} """, ) val javaLibraryModule2 = JavaSourceFile( "LibraryModule2.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class LibraryModule2 extends LibraryGlideModule {} """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule1, kotlinLibraryModule2, javaAppModule, javaLibraryModule1, javaLibraryModule2, ) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithMultipleLibraryModules) assertThat(it.exitCode).isEqualTo(ExitCode.OK) } } @Test fun compile_withTheSameLibraryGlideModuleInMultipleFiles_andAnAppGlideModule_generatesGeneratedAppGlideModuleThatCallsTheLibraryGlideModuleOnce() { // Kotlin seems fine with multiple identical classes. For Java this is compile time error // already, so we don't have to handle it. assumeTrue(sourceType == SourceType.KOTLIN) val kotlinLibraryModule1 = KotlinSourceFile( "LibraryModule1.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinLibraryModule2 = KotlinSourceFile( "LibraryModule2.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """, ) compileCurrentSourceType(kotlinAppModule, kotlinLibraryModule1, kotlinLibraryModule2) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithLibraryModule) assertThat(it.exitCode).isEqualTo(ExitCode.OK) assertThat(it.messages) .contains( GlideSymbolProcessorConstants.DUPLICATE_LIBRARY_MODULE_ERROR.format("[LibraryModule]") ) } } @Test fun compile_withLibraryGlideModulesWithDifferentPackages_butSameName_andAppGlideModule_callsEachLibraryGlideModuleOnceFromGeneratedAppGlideModule() { // TODO(judds): The two java classes don't compile when run by the annotation processor, which // means we can't really test this case for java code. Fix compilation issue and re-enable this // test for Java code. assumeTrue(sourceType == SourceType.KOTLIN) val kotlinLibraryModule1 = KotlinSourceFile( "LibraryModule1.kt", """ package first_package import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinLibraryModule2 = KotlinSourceFile( "LibraryModule2.kt", """ package second_package import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """, ) val javaLibraryModule1 = JavaSourceFile( "LibraryModule1.java", """ package first_package; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; public class LibraryModule1 { @GlideModule public static final class LibraryModule extends LibraryGlideModule {} } """, ) val javaLibraryModule2 = JavaSourceFile( "LibraryModule2.java", """ package second_package; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; public class LibraryModule2 { @GlideModule public static final class LibraryModule extends LibraryGlideModule {} } """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule1, kotlinLibraryModule2, javaAppModule, javaLibraryModule1, javaLibraryModule2, ) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithPackagePrefixedLibraryModules) assertThat(it.exitCode).isEqualTo(ExitCode.OK) } } @Test fun compile_withLibraryModuleInExcludes_producesGeneratedAppGlideModuleThatDoesNotCallExcludedLibraryModule() { val kotlinLibraryModule1 = KotlinSourceFile( "LibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinLibraryModule2 = KotlinSourceFile( "ExcludedLibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class ExcludedLibraryModule : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.Excludes import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule @Excludes(ExcludedLibraryModule::class) class AppModule : AppGlideModule() """, ) val javaLibraryModule1 = JavaSourceFile( "LibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class LibraryModule extends LibraryGlideModule {} """, ) val javaLibraryModule2 = JavaSourceFile( "ExcludedLibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class ExcludedLibraryModule extends LibraryGlideModule {} """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule @Excludes(ExcludedLibraryModule.class) public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule1, kotlinLibraryModule2, javaAppModule, javaLibraryModule1, javaLibraryModule2, ) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithLibraryModule) assertThat(it.exitCode).isEqualTo(ExitCode.OK) } } @Test fun compile_withMultipleLibraryModulesInExcludes_producesGeneratedAppGlideModuleThatDoesNotCallExcludedLibraryModules() { val kotlinLibraryModule1 = KotlinSourceFile( "LibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val kotlinLibraryModule2 = KotlinSourceFile( "ExcludedLibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class ExcludedLibraryModule : LibraryGlideModule() """, ) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.Excludes import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule @Excludes(LibraryModule::class, ExcludedLibraryModule::class) class AppModule : AppGlideModule() """, ) val javaLibraryModule1 = JavaSourceFile( "LibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class LibraryModule extends LibraryGlideModule {} """, ) val javaLibraryModule2 = JavaSourceFile( "ExcludedLibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class ExcludedLibraryModule extends LibraryGlideModule {} """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule @Excludes({LibraryModule.class, ExcludedLibraryModule.class}) public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType( kotlinAppModule, kotlinLibraryModule1, kotlinLibraryModule2, javaAppModule, javaLibraryModule1, javaLibraryModule2, ) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(CommonSources.simpleAppGlideModule) assertThat(it.exitCode).isEqualTo(ExitCode.OK) } } @Test fun compile_withAppModuleWithEmptyExcludes_fails() { val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.Excludes import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule @Excludes class AppModule : AppGlideModule() """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule @Excludes public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType(kotlinAppModule, javaAppModule) { assertThat(it.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) assertThat(it.messages) .contains(AppGlideModuleConstants.INVALID_EXCLUDES_ANNOTATION_MESSAGE.format("AppModule")) } } @Test fun compile_withAppModuleWithExcludes_pointingToAppModules_fails() { val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.Excludes import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule class SomeOtherAppModule: AppGlideModule() @GlideModule @Excludes(SomeOtherAppModule::class) class AppModule : AppGlideModule() """, ) val otherJavaAppModule = JavaSourceFile( "SomeOtherAppModule.java", """ import com.bumptech.glide.module.AppGlideModule; public class SomeOtherAppModule extends AppGlideModule { public SomeOtherAppModule() {} } """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.Excludes; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule @Excludes(SomeOtherAppModule.class) public class AppModule extends AppGlideModule { public AppModule() {} } """, ) compileCurrentSourceType(kotlinAppModule, otherJavaAppModule, javaAppModule) { assertThat(it.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) assertThat(it.messages) .contains(AppGlideModuleConstants.INVALID_EXCLUDES_ANNOTATION_MESSAGE.format("AppModule")) } } @Test fun compile_withLibraryGlideModule_compiledSeparately_includesLibraryGlideModule_2() { val kotlinLibraryModule = KotlinSourceFile( "LibraryModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.LibraryGlideModule @GlideModule class LibraryModule : LibraryGlideModule() """, ) val javaLibraryModule = JavaSourceFile( "LibraryModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.LibraryGlideModule; @GlideModule public class LibraryModule extends LibraryGlideModule {} """, ) val libraryCompilationResult = compileCurrentSourceType(kotlinLibraryModule, javaLibraryModule) val kotlinAppModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """, ) val javaAppModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} } """, ) val generatedLibrarySources = libraryCompilationResult.allGeneratedFiles().map { GeneratedSourceFile(it, sourceType) } compileCurrentSourceType( *(listOf(kotlinAppModule, javaAppModule) + generatedLibrarySources).toTypedArray() ) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(appGlideModuleWithLibraryModule) } } } // generated code always includes public and Unit @Suppress("RedundantVisibilityModifier", "RedundantUnitReturnType") @Language("kotlin") const val appGlideModuleWithPackagePrefixedLibraryModules = """ package com.bumptech.glide import AppModule import android.content.Context import first_package.LibraryModule import kotlin.Boolean import kotlin.Suppress import kotlin.Unit internal class GeneratedAppGlideModuleImpl( @Suppress("UNUSED_PARAMETER") context: Context, ) : GeneratedAppGlideModule() { private val appGlideModule: AppModule init { appGlideModule = AppModule() } public override fun registerComponents( context: Context, glide: Glide, registry: Registry, ): Unit { LibraryModule().registerComponents(context, glide, registry) second_package.LibraryModule().registerComponents(context, glide, registry) appGlideModule.registerComponents(context, glide, registry) } public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { appGlideModule.applyOptions(context, builder) } public override fun isManifestParsingEnabled(): Boolean = false } """ // generated code always includes public and Unit @Suppress("RedundantVisibilityModifier", "RedundantUnitReturnType") @Language("kotlin") const val appGlideModuleWithLibraryModule = """ package com.bumptech.glide import AppModule import LibraryModule import android.content.Context import kotlin.Boolean import kotlin.Suppress import kotlin.Unit internal class GeneratedAppGlideModuleImpl( @Suppress("UNUSED_PARAMETER") context: Context, ) : GeneratedAppGlideModule() { private val appGlideModule: AppModule init { appGlideModule = AppModule() } public override fun registerComponents( context: Context, glide: Glide, registry: Registry, ): Unit { LibraryModule().registerComponents(context, glide, registry) appGlideModule.registerComponents(context, glide, registry) } public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { appGlideModule.applyOptions(context, builder) } public override fun isManifestParsingEnabled(): Boolean = false } """ // generated code always includes public and Unit @Suppress("RedundantVisibilityModifier", "RedundantUnitReturnType") @Language("kotlin") const val appGlideModuleWithMultipleLibraryModules = """ package com.bumptech.glide import AppModule import LibraryModule1 import LibraryModule2 import android.content.Context import kotlin.Boolean import kotlin.Suppress import kotlin.Unit internal class GeneratedAppGlideModuleImpl( @Suppress("UNUSED_PARAMETER") context: Context, ) : GeneratedAppGlideModule() { private val appGlideModule: AppModule init { appGlideModule = AppModule() } public override fun registerComponents( context: Context, glide: Glide, registry: Registry, ): Unit { LibraryModule1().registerComponents(context, glide, registry) LibraryModule2().registerComponents(context, glide, registry) appGlideModule.registerComponents(context, glide, registry) } public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { appGlideModule.applyOptions(context, builder) } public override fun isManifestParsingEnabled(): Boolean = false } """ ================================================ FILE: annotation/ksp/test/src/test/kotlin/com/bumptech/glide/annotation/ksp/test/OnlyAppGlideModuleTests.kt ================================================ package com.bumptech.glide.annotation.ksp.test import com.bumptech.glide.annotation.ksp.AppGlideModuleConstants import com.bumptech.glide.annotation.ksp.GlideSymbolProcessorConstants import com.google.common.truth.Truth.assertThat import com.tschuchort.compiletesting.KotlinCompilation import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @RunWith(Parameterized::class) @OptIn(ExperimentalCompilerApi::class) class OnlyAppGlideModuleTests(override val sourceType: SourceType) : PerSourceTypeTest { companion object { @Parameterized.Parameters(name = "sourceType = {0}") @JvmStatic fun data() = SourceType.values() } @Test fun compile_withGlideModuleOnNonLibraryClass_fails() { val kotlinSource = KotlinSourceFile( "Something.kt", """ import com.bumptech.glide.annotation.GlideModule @GlideModule class Something """ ) val javaSource = JavaSourceFile( "Something.java", """ package test; import com.bumptech.glide.annotation.GlideModule; @GlideModule public class Something {} """ ) compileCurrentSourceType(kotlinSource, javaSource) { assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(it.messages) .containsMatch( GlideSymbolProcessorConstants.INVALID_ANNOTATED_CLASS.format(".*/Something.*") ) } } @Test fun compile_withGlideModuleOnValidAppGlideModule_generatedGeneratedAppGlideModule() { val kotlinModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule : AppGlideModule() """ ) val javaModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule {} """ .trimIndent() ) compileCurrentSourceType(kotlinModule, javaModule) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(CommonSources.simpleAppGlideModule) assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) } } @Test fun compile_withGlideModuleOnValidAppGlideModuleThroughBaseClass_generatedGeneratedAppGlideModule() { val kotlinModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule class BaseAppModule : AppGlideModule() @GlideModule class AppModule : BaseAppModule() """ ) val javaBaseAppModule = JavaSourceFile( "BaseAppModule.java", """ import com.bumptech.glide.module.AppGlideModule; public class BaseAppModule extends AppGlideModule {} """ .trimIndent() ) val javaModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; @GlideModule public class AppModule extends BaseAppModule {} """ .trimIndent() ) compileCurrentSourceType(kotlinModule, javaBaseAppModule, javaModule) { assertThat(it.generatedAppGlideModuleContents()) .hasSourceEqualTo(CommonSources.simpleAppGlideModule) assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) } } @Test fun compile_withAppGlideModuleConstructorAcceptingOnlyContext_generatesGeneratedAppGlideModule() { val kotlinModule = KotlinSourceFile( "AppModule.kt", """ import android.content.Context import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule(context: Context) : AppGlideModule() """ ) val javaModule = JavaSourceFile( "AppModule.java", """ import android.content.Context; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule(Context context) {} } """ ) compileCurrentSourceType(kotlinModule, javaModule) { assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(appGlideModuleWithContext) assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) } } @Test fun compile_withAppGlideModuleConstructorRequiringOtherThanContext_fails() { val kotlinModule = KotlinSourceFile( "AppModule.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule(value: Int) : AppGlideModule() """ ) val javaModule = JavaSourceFile( "AppModule.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule(Integer value) {} } """ ) compileCurrentSourceType(kotlinModule, javaModule) { assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(it.messages).contains(AppGlideModuleConstants.INVALID_MODULE_MESSAGE) } } @Test fun compile_withAppGlideModuleConstructorRequiringMultipleArguments_fails() { val kotlinModule = KotlinSourceFile( "AppModule.kt", """ import android.content.Context import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule(value: Context, otherValue: Int) : AppGlideModule() """ ) val javaModule = JavaSourceFile( "AppModule.java", """ import android.content.Context; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class AppModule extends AppGlideModule { public AppModule(Context value, int otherValue) {} } """ ) compileCurrentSourceType(kotlinModule, javaModule) { assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(it.messages).contains(AppGlideModuleConstants.INVALID_MODULE_MESSAGE) } } // This is quite weird, we could probably pretty reasonably just assert that this doesn't happen. @Test fun compile_withAppGlideModuleWithOneEmptyConstructor_andOneContextOnlyConstructor_usesTheContextOnlyConstructor() { val kotlinModule = KotlinSourceFile( "AppModule.kt", """ import android.content.Context import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class AppModule(context: Context?) : AppGlideModule() { constructor() : this(null) } """ ) val javaModule = JavaSourceFile( "AppModule.java", """ import android.content.Context; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; import javax.annotation.Nullable; @GlideModule public class AppModule extends AppGlideModule { public AppModule() {} public AppModule(@Nullable Context context) {} } """ ) compileCurrentSourceType(kotlinModule, javaModule) { assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) assertThat(it.generatedAppGlideModuleContents()).hasSourceEqualTo(appGlideModuleWithContext) } } @Test fun compile_withMultipleAppGlideModules_fails() { val firstKtModule = KotlinSourceFile( "Module1.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class Module1 : AppGlideModule() """ ) val secondKtModule = KotlinSourceFile( "Module2.kt", """ import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class Module2 : AppGlideModule() """ ) val firstJavaModule = JavaSourceFile( "Module1.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class Module1 extends AppGlideModule { public Module1() {} } """ ) val secondJavaModule = JavaSourceFile( "Module2.java", """ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class Module2 extends AppGlideModule { public Module2() {} } """ ) compileCurrentSourceType(firstKtModule, secondKtModule, firstJavaModule, secondJavaModule) { assertThat(it.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(it.messages) .contains( GlideSymbolProcessorConstants.SINGLE_APP_MODULE_ERROR.format("[Module1, Module2]") ) } } } @Language("kotlin") const val appGlideModuleWithContext = """ package com.bumptech.glide import AppModule import android.content.Context import kotlin.Boolean import kotlin.Unit internal class GeneratedAppGlideModuleImpl( context: Context, ) : GeneratedAppGlideModule() { private val appGlideModule: AppModule init { appGlideModule = AppModule(context) } public override fun registerComponents( context: Context, glide: Glide, registry: Registry, ): Unit { appGlideModule.registerComponents(context, glide, registry) } public override fun applyOptions(context: Context, builder: GlideBuilder): Unit { appGlideModule.applyOptions(context, builder) } public override fun isManifestParsingEnabled(): Boolean = false } """ ================================================ FILE: annotation/src/main/java/com/bumptech/glide/annotation/Excludes.java ================================================ package com.bumptech.glide.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Specifies a set of GlideModule and/or LibraryGlideModule classes that should be excluded from an * application. * *

Used only on AppGlideModules. Adding this annotation to other classes will have no affect. * *

Cannot be used to exclude AppGlideModules (there must be at most one per Application anyway). */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Excludes { Class[] value(); } ================================================ FILE: annotation/src/main/java/com/bumptech/glide/annotation/GlideExtension.java ================================================ package com.bumptech.glide.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicate a class that extends Glide's public API. * * @see GlideOption */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface GlideExtension {} ================================================ FILE: annotation/src/main/java/com/bumptech/glide/annotation/GlideModule.java ================================================ package com.bumptech.glide.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Identifies AppGlideModules and LibraryGlideModules for Glide's annotation processor to merge at * compile time. * *

Replaces {@code } tags in AndroidManifest.xml. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface GlideModule { /** * Returns the name of the class that will be used as a replacement for {@code * com.bumptech.glide.Glide} in Applications that depend on Glide's generated code. */ String glideName() default "GlideApp"; } ================================================ FILE: annotation/src/main/java/com/bumptech/glide/annotation/GlideOption.java ================================================ package com.bumptech.glide.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Identifies methods in {@link GlideExtension} annotated classes that extend {@code * com.bumptech.glide.request.RequestOptions}. * *

All annotated methods will be added to a single {@code * com.bumptech.glide.request.RequestOptions} implementation generated per application. Overlapping * method names in different extensions may cause errors at compile time. * *

Static equivalents of annotated methods will also be generated. * *

Methods with this annotation will only be found if they belong to classes annotated with * {@link GlideExtension}. * *

The preferred way of writing extension methods returns the provided {@code * com.bumptech.glide.request.RequestOptions} object with one or more methods called on it. You must * not return a newly instantiated {@code com.bumptech.glide.request.RequestOptions} object as doing * so my cause a {@code ClassCastException} at runtime. Calling either {@code * com.bumptech.glide.request.RequestOptions#autoClone()} or {@code * com.bumptech.glide.request.RequestOptions#lock()} is safe, but unnecessary and should typically * be avoided. The preferred style looks like: * *

{@code
 * {@link @}GlideExtension
 * public class MyExtension {
 *   private MyExtension() {}
 *
 *   {@literal @}GlideOption
 *   public static RequestOptions myOption(RequestOptions options) {
 *     return options
 *         .optionOne()
 *         .optionTwo();
 *   }
 * }
 * }
* *

The deprecated way of writing extension methods is simply a static void method. The {@code * com.bumptech.glide.request.RequestOptions} object is cloned before it is passed to this method to * avoid an option method returning a new instance, but using methods like {@code * com.bumptech.glide.request.RequestOptions#clone()} or {@code * com.bumptech.glide.request.RequestOptions#autoClone()} can result in options applied in the * method being silently ignored. Prefer the new style whenever possible. * *

{@code
 * {@literal @}GlideExtension
 * public class MyExtension {
 *   private MyExtension() {}
 *
 *   // Deprecated! Use the new style of GlideOption extensions instead.
 *   {@literal @}GlideOption
 *   public static void myOption(RequestOptions options) {
 *     options
 *         .optionOne()
 *         .optionTwo();
 *   }
 * }
 * }
*/ @Target(ElementType.METHOD) // Needs to be parsed from class files in JAR. @Retention(RetentionPolicy.CLASS) public @interface GlideOption { /** Does not intend to override a method in a super class. */ int OVERRIDE_NONE = 0; /** Expects to call super and then add additional functionality to an overridden method. */ int OVERRIDE_EXTEND = 1; /** Expects to not call super and replace an overridden method. */ int OVERRIDE_REPLACE = 2; /** * Determines how and whether a generated method should extend a method from it's parent. * *

Must be one of {@link #OVERRIDE_NONE}, {@link #OVERRIDE_EXTEND}, {@link #OVERRIDE_REPLACE}. * *

The extended method is determined by String and argument matching against methods in the * extended class. If {@link #OVERRIDE_NONE} is used and the method and arguments match a method * in the extended class, a compile time error will result. Similarly if any other override type * is used and no method/arguments in the extended class match, a compile time error will result. */ int override() default OVERRIDE_NONE; /** * Sets the name for the generated static version of this method. * *

If this value is not set, the static method name is just the original method name with "Of" * appended. */ String staticMethodName() default ""; /** * {@code true} to indicate that it's safe to statically memoize the result of this method using * {@code com.bumptech.glide.request.RequestOptions#autoClone()}. * *

This method should only be used for no-arg methods where there's only a single possible * value. * *

Memoization can save object allocations for frequently used options. */ boolean memoizeStaticMethod() default false; /** * {@code true} to prevent a static builder method from being generated. * *

By default static methods are generated for all methods annotated with {@link GlideOption}. * These static factory methods allow for a cleaner API when used with {@code * com.bumptech.glide.RequestBuilder#apply}. The static factory method by default simply creates a * new {@code com.bumptech.glide.request.RequestOptions} object, calls the instance version of the * method on it and returns it. For example: * *

   * 
   * public static GlideOptions noAnimation() {
   *   return new GlideOptions().dontAnimate();
   * }
   * 
   * 
* * @see #memoizeStaticMethod() * @see #staticMethodName() */ boolean skipStaticMethod() default false; } ================================================ FILE: annotation/src/main/java/com/bumptech/glide/annotation/GlideType.java ================================================ package com.bumptech.glide.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Identifies methods in {@link GlideExtension} annotated classes that extend {@code * com.bumptech.glide.RequestManager}. * *

If one or more method is found with this annotation, an additional API entry point that * exposes a generated {@code com.bumptech.glide.RequestManager} subclass will be created. The * generated API entry point acts as a drop in replacement for Glide. Glide.with(fragment) becomes * GlideApp.with(fragment). Although the Glide.with variant will still be available, only the new * API entry point will provide access to these additional methods. * *

The name of the API entry point created when one of these methods is found can be controlled * by {@link GlideModule#glideName()}. * *

Methods with this annotation will only be found if they are contained in a class annotated * with {@link GlideExtension}. * *

Methods annotated with GlideType must have a single parameter. The type of the single * parameter must be {@code com.bumptech.glide.RequestBuilder}, with a type matching the value of * {@link #value()}. * *

Compilation will fail if a method annotated with this method is identical to a method in * {@code com.bumptech.glide.RequestManager} */ @Target(ElementType.METHOD) // Needs to be parsed from class files in JAR. @Retention(RetentionPolicy.CLASS) public @interface GlideType { /** * A Resource class name, like GifDrawable.class, Bitmap.class etc. * *

Must match the type of the {@code com.bumptech.glide.RequestBuilder} parameter in the * annotated method. */ Class value(); } ================================================ FILE: annotation/src/main/java/com/bumptech/glide/annotation/compiler/Index.java ================================================ package com.bumptech.glide.annotation.compiler; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Used to retrieve LibraryGlideModule and GlideExtension classes in our annotation processor from * libraries and applications. * *

Part of the internals of Glide's annotation processor and not for public use. */ @Target(ElementType.TYPE) // Needs to be parsed from class files in JAR. @Retention(RetentionPolicy.CLASS) @interface Index { String[] modules() default {}; String[] extensions() default {}; } ================================================ FILE: annotation/src/main/java/com/bumptech/glide/annotation/ksp/Index.java ================================================ package com.bumptech.glide.annotation.ksp; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Used to retrieve LibraryGlideModule and GlideExtension classes in our annotation processor from * libraries and applications. * *

Part of the internals of Glide's annotation processor and not for public use. */ @Target(ElementType.TYPE) // Needs to be parsed from class files in JAR. @Retention(RetentionPolicy.CLASS) @interface Index { String[] modules() default {}; } ================================================ FILE: benchmark/benchmark-proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile -dontobfuscate -ignorewarnings -keepattributes *Annotation* -dontnote junit.framework.** -dontnote junit.runner.** -dontwarn androidx.test.** -dontwarn org.junit.** -dontwarn org.hamcrest.** -dontwarn com.squareup.javawriter.JavaWriter -keepclasseswithmembers @org.junit.runner.RunWith public class * ================================================ FILE: benchmark/build.gradle.kts ================================================ plugins { id("com.android.library") id("androidx.benchmark") } android { namespace = "com.bumptech.glide.benchmark" compileSdk = 34 buildToolsVersion = "34.0.0" compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } defaultConfig { minSdk = 19 testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner" multiDexEnabled = true } buildTypes { getByName("debug") { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" ) } } } dependencies { implementation(libs.androidx.multidex) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) androidTestImplementation(project(":library")) androidTestImplementation(project(":testutil")) androidTestImplementation(libs.androidx.benchmark.junit) androidTestImplementation(libs.guava) } ================================================ FILE: benchmark/gradle.properties ================================================ android.enableAdditionalTestOutput=true ================================================ FILE: benchmark/src/androidTest/AndroidManifest.xml ================================================ ================================================ FILE: benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkData.java ================================================ package com.bumptech.glide.benchmark; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.benchmark.GlideBenchmarkRule.AfterStep; import com.bumptech.glide.benchmark.GlideBenchmarkRule.BeforeStep; import com.bumptech.glide.benchmark.GlideBenchmarkRule.LoadStep; import com.bumptech.glide.benchmark.data.DataOpener; import com.bumptech.glide.benchmark.data.DataOpener.ByteArrayBufferOpener; import com.bumptech.glide.benchmark.data.DataOpener.ParcelFileDescriptorOpener; import com.bumptech.glide.benchmark.data.DataOpener.StreamOpener; import com.bumptech.glide.testutil.MockModelLoader; import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class BenchmarkData { private final int smallResourceId = R.raw.small; private final int hugeHeaderResourceId = R.raw.huge_header; @Rule public final GlideBenchmarkRule glideBenchmarkRule = new GlideBenchmarkRule(); @Test public void smallAsStream() throws Exception { benchmarkData(new StreamOpener(), smallResourceId); } @Test public void hugeHeaderAsStream() throws Exception { benchmarkData(new StreamOpener(), hugeHeaderResourceId); } @Test public void smallAsByteArrayBuffer() throws Exception { benchmarkData(new ByteArrayBufferOpener(), smallResourceId); } @Test public void hugeHeaderAsByteArrayBuffer() throws Exception { benchmarkData(new ByteArrayBufferOpener(), hugeHeaderResourceId); } @Test public void smallAsFileDescriptor() throws Exception { benchmarkData(new ParcelFileDescriptorOpener(), smallResourceId); } @Test public void hugeHeaderAsFileDescriptor() throws Exception { benchmarkData(new ParcelFileDescriptorOpener(), hugeHeaderResourceId); } static final class ModelAndData { private final Object model; private final DataT data; ModelAndData(Object model, DataT data) { this.model = model; this.data = data; } } private void benchmarkData(final DataOpener opener, final int resourceId) throws Exception { glideBenchmarkRule.runBenchmark( new BeforeStep>() { @Override public ModelAndData act() throws IOException { FakeModel fakeModel = new FakeModel(); T data = opener.acquire(resourceId); MockModelLoader.mock(fakeModel, data); return new ModelAndData(fakeModel, data); } }, new LoadStep>() { @Override public Object getModel(ModelAndData beforeData) { return beforeData.model; } }, new AfterStep>() { @Override public void act(ModelAndData beforeData) throws IOException { opener.close(beforeData.data); } }); } private static final class FakeModel {} } ================================================ FILE: benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkFromCache.java ================================================ package com.bumptech.glide.benchmark; import android.app.Application; import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import androidx.benchmark.BenchmarkState; import androidx.benchmark.junit4.BenchmarkRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.Glide; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.google.common.base.Preconditions; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** Simulate loading a file from Glide's cache as a thumbnail in various sizes. */ @RunWith(AndroidJUnit4.class) public class BenchmarkFromCache { private final ConcurrencyHelper concurrencyHelper = new ConcurrencyHelper(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); @Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule(); private final Application app = ApplicationProvider.getApplicationContext(); @Test public void pixel3a_portrait_original() throws Exception { runBenchmark(R.raw.pixel3a_portrait, Target.SIZE_ORIGINAL); } @Test public void pixel3a_portrait_large() throws Exception { runBenchmark(R.raw.pixel3a_portrait, 2048); } @Test public void pixel3a_portrait_medium() throws Exception { runBenchmark(R.raw.pixel3a_portrait, 1024); } @Test public void pixel3a_portrait_small() throws Exception { runBenchmark(R.raw.pixel3a_portrait, 256); } @Test public void pixel3a_portrait_tiny() throws Exception { runBenchmark(R.raw.pixel3a_portrait, 50); } private void runBenchmark(@RawRes final int resourceId, final int targetSize) throws Exception { BenchmarkState state = benchmarkRule.getState(); state.pauseTiming(); // Writes to the disk cache happen asynchronously after a request completes. To make sure we're // only timing reads from disk cache, we need to make sure we wait until that async write // finishes. This is a simple, albiet hacky, way to accomplish that. try { while (true) { loadImageWithExpectedDataSource( state, resourceId, targetSize, DataSource.LOCAL, /* isAlreadyPaused= */ true); } } catch (IllegalStateException e) { // Now that we're no longer getting LOCAL as our data source, it's safe to proceed. } state.resumeTiming(); while (state.keepRunning()) { state.pauseTiming(); clearMemoryCache(); state.resumeTiming(); loadImageWithExpectedDataSource( state, resourceId, targetSize, DataSource.RESOURCE_DISK_CACHE, /* isAlreadyPaused= */ false); } } private void loadImageWithExpectedDataSource( BenchmarkState state, @RawRes int resourceId, int targetSize, DataSource expectedDataSource, boolean isAlreadyPaused) throws InterruptedException, ExecutionException, TimeoutException { final AtomicReference dataSourceRef = new AtomicReference<>(); FutureTarget target = Glide.with(app) .asBitmap() .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .skipMemoryCache(true) .override(targetSize) .load(resourceId) .listener( new RequestListener() { @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( @NonNull Bitmap resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { dataSourceRef.set(dataSource); return false; } }) .submit(); target.get(15, TimeUnit.SECONDS); if (!isAlreadyPaused) { state.pauseTiming(); } Preconditions.checkState(dataSourceRef.get() == expectedDataSource, dataSourceRef.get()); Glide.with(app).clear(target); if (!isAlreadyPaused) { state.resumeTiming(); } } private void clearMemoryCache() { concurrencyHelper.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(app).clearMemory(); } }); } } ================================================ FILE: benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkMediaStoreData.java ================================================ package com.bumptech.glide.benchmark; import android.Manifest.permission; import android.app.Application; import android.content.pm.PackageManager; import android.net.Uri; import androidx.benchmark.BenchmarkState; import androidx.benchmark.junit4.BenchmarkRule; import androidx.core.content.ContextCompat; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.base.Preconditions; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.Callable; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** * Benchmarks that do not involve Glide just to figure out how media store is behaving on a given * device. */ @RunWith(AndroidJUnit4.class) public class BenchmarkMediaStoreData { // Pick a media store Uri on the device that you know has lat/lng. private static final Uri MEDIA_STORE_URI = Uri.parse("content://media/external/images/media/194"); private final Application app = ApplicationProvider.getApplicationContext(); @Rule private final BenchmarkRule benchmarkRule = new BenchmarkRule(); @Before public void setUp() { benchmarkRule.getState().pauseTiming(); Preconditions.checkState( ContextCompat.checkSelfPermission(app, permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED); benchmarkRule.getState().resumeTiming(); } @Test public void readCacheFileFully() throws Exception { benchmarkRule.getState().pauseTiming(); InputStream is = null; OutputStream os = null; final File file; try { is = app.getContentResolver().openInputStream(MEDIA_STORE_URI); file = File.createTempFile("tempBenchmarkModel", "jpg", app.getCacheDir()); os = new FileOutputStream(file); byte[] buffer = new byte[1024 * 1024]; int read; while ((read = is.read(buffer, 0, buffer.length)) != -1) { os.write(buffer, 0, read); } os.close(); } finally { if (is != null) { is.close(); } if (os != null) { os.close(); } } benchmarkRule.getState().resumeTiming(); BenchmarkState state = benchmarkRule.getState(); while (state.keepRunning()) { readFully( new Callable() { @Override public InputStream call() throws Exception { return new FileInputStream(file); } }); } } @Test public void readMediaStoreFileFully() throws Exception { BenchmarkState state = benchmarkRule.getState(); while (state.keepRunning()) { readFully( new Callable() { @Override public InputStream call() throws Exception { return app.getContentResolver().openInputStream(MEDIA_STORE_URI); } }); } } private void readFully(Callable openInputStream) throws Exception { InputStream is = null; try { is = openInputStream.call(); byte[] buffer = new byte[1024 * 1024]; while (is.read(buffer, 0, buffer.length) != -1) { // Continue } } finally { if (is != null) { is.close(); } } } } ================================================ FILE: benchmark/src/androidTest/java/com/bumptech/glide/benchmark/BenchmarkModels.java ================================================ package com.bumptech.glide.benchmark; import android.app.Application; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; import androidx.annotation.RawRes; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.benchmark.GlideBenchmarkRule.AfterStep; import com.bumptech.glide.benchmark.GlideBenchmarkRule.BeforeStep; import com.bumptech.glide.benchmark.data.DataOpener.FileOpener; import com.google.common.base.Preconditions; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** * Tests that simulate the complete Glide flow by providing supported Model types directly to Glide. * *

While these benchmarks most directly mimic Glide's current behavior, they have some additional * noise due to the large number of steps involved. Other benchmarks in this project may be more * targeted and provide more accurate results for smaller tweaks. */ @RunWith(AndroidJUnit4.class) public class BenchmarkModels { private final Application app = ApplicationProvider.getApplicationContext(); private final int smallResourceId = R.raw.small; private final int hugeHeaderResourceId = R.raw.huge_header; @Rule public final GlideBenchmarkRule glideBenchmarkRule = new GlideBenchmarkRule(); @Test public void smallAsCacheFile() throws Exception { benchmarkAsCacheFile(smallResourceId); } @Test public void hugeHeaderAsCacheFile() throws Exception { benchmarkAsCacheFile(hugeHeaderResourceId); } @Test public void smallAsResourceId() throws Exception { benchmarkModel(smallResourceId); } @Test public void hugeHeaderAsResourceId() throws Exception { benchmarkModel(hugeHeaderResourceId); } @Test public void smallAsResourceUri() throws Exception { Uri uri = resourceUriFromId(smallResourceId); benchmarkModel(uri); } @Test public void hugeHeaderAsResourceUri() throws Exception { Uri uri = resourceUriFromId(hugeHeaderResourceId); benchmarkModel(uri); } @Test public void smallAsMediaStoreUri() throws Exception { benchmarkAsMediaStoreUri(smallResourceId); } @Test public void hugeHeaderAsMediaStoreUri() throws Exception { benchmarkAsMediaStoreUri(hugeHeaderResourceId); } @Test public void pixel3aAsMediaStoreUri() throws Exception { benchmarkAsMediaStoreUri(R.raw.pixel3a_portrait); } @Test public void pixel3aExifRotatedAsMediaStoreUri() throws Exception { benchmarkAsMediaStoreUri(R.raw.pixel3a_exif_rotated); } @Test public void pixel3aMvimgExifRotatedAsMediaStoreUri() throws Exception { benchmarkAsMediaStoreUri(R.raw.pixel3a_mvimg_exif_rotated); } @Test public void smallAsMediaStoreFilepath() throws Exception { benchmarkAsMediaStoreFilepath(smallResourceId); } @Test public void pixel3aAsMediaStoreFilepath() throws Exception { benchmarkAsMediaStoreFilepath(R.raw.pixel3a_portrait); } @Test public void pixel3aExifRotatedAsMediaStoreFilepath() throws Exception { benchmarkAsMediaStoreFilepath(R.raw.pixel3a_exif_rotated); } @Test public void pixel3aMvimgExifRotatedAsMediaStoreFilepath() throws Exception { benchmarkAsMediaStoreFilepath(R.raw.pixel3a_mvimg_exif_rotated); } @Test public void hugeHeaderAsMediaStoreFilepath() throws Exception { benchmarkAsMediaStoreFilepath(hugeHeaderResourceId); } private Uri resourceUriFromId(@RawRes int resourceId) { glideBenchmarkRule.pauseTiming(); try { return new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(app.getPackageName()) .appendPath(app.getResources().getResourceTypeName(resourceId)) .appendPath(app.getResources().getResourceEntryName(resourceId)) .build(); } finally { glideBenchmarkRule.resumeTiming(); } } private Uri mediaStoreUriFromId(@RawRes int resourceId) throws IOException { glideBenchmarkRule.pauseTiming(); try { Uri mediaStoreUri = app.getContentResolver() .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues()); InputStream is = null; OutputStream os = null; try { is = app.getResources().openRawResource(resourceId); os = app.getContentResolver().openOutputStream(mediaStoreUri); byte[] buffer = new byte[1024 * 1024]; int read; while ((read = is.read(buffer, /* off= */ 0, buffer.length)) != -1) { os.write(buffer, /* off= */ 0, read); } // Make sure we actually write all of the data or fail by throwing immediately. os.close(); return mediaStoreUri; } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } if (os != null) { try { os.close(); } catch (IOException e) { // Ignored. } } } } finally { glideBenchmarkRule.resumeTiming(); } } private void benchmarkAsMediaStoreUri(@RawRes int resourceId) throws Exception { Uri mediaStoreUri = mediaStoreUriFromId(resourceId); try { benchmarkModel(mediaStoreUri); } finally { cleanupMediaStoreUri(mediaStoreUri); } } private void cleanupMediaStoreUri(Uri mediaStoreUri) { glideBenchmarkRule.pauseTiming(); int result = app.getContentResolver().delete(mediaStoreUri, /* extras= */ null); Preconditions.checkState(result == 1); glideBenchmarkRule.resumeTiming(); } private void benchmarkAsMediaStoreFilepath(@RawRes int resourceId) throws Exception { Uri mediaStoreUri = mediaStoreUriFromId(resourceId); try { benchmarkModel(getMediaStoreFilepath(mediaStoreUri)); } finally { cleanupMediaStoreUri(mediaStoreUri); } } private String getMediaStoreFilepath(Uri mediaStoreUri) { glideBenchmarkRule.pauseTiming(); String[] projection = new String[] {MediaStore.Images.Media.DATA}; Cursor cursor = app.getContentResolver() .query( mediaStoreUri, projection, /* selection= */ null, /* selectionArgs= */ null, /* sortOrder= */ null); try { Preconditions.checkState(cursor.moveToFirst()); return cursor.getString(0); } finally { cursor.close(); glideBenchmarkRule.resumeTiming(); } } private void benchmarkAsCacheFile(@RawRes final int resourceId) throws Exception { final FileOpener fileOpener = new FileOpener(); glideBenchmarkRule.runBenchmark( new BeforeStep() { @Override public File act() throws IOException { return fileOpener.acquire(resourceId); } }, new AfterStep() { @Override public void act(File beforeData) { fileOpener.close(beforeData); } }); } private void benchmarkModel(final Object model) throws Exception { glideBenchmarkRule.runBenchmark( new BeforeStep() { @Override public Object act() { return model; } }, new AfterStep() { @Override public void act(Object beforeData) {} }); } } ================================================ FILE: benchmark/src/androidTest/java/com/bumptech/glide/benchmark/GlideBenchmarkRule.java ================================================ package com.bumptech.glide.benchmark; import android.content.Context; import androidx.benchmark.BenchmarkState; import androidx.benchmark.junit4.BenchmarkRule; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.testutil.TearDownGlide; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; import org.junit.rules.RuleChain; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; final class GlideBenchmarkRule implements TestRule { private final TearDownGlide tearDownGlide = new TearDownGlide(); private final BenchmarkRule benchmarkRule = new BenchmarkRule(); private final TestRule ruleChain = RuleChain.outerRule(benchmarkRule).around(tearDownGlide); @NotNull @Override public Statement apply(@NotNull Statement base, @NotNull Description description) { return ruleChain.apply(base, description); } void pauseTiming() { benchmarkRule.getState().pauseTiming(); } void resumeTiming() { benchmarkRule.getState().resumeTiming(); } BenchmarkRule getBenchmark() { return benchmarkRule; } interface LoadStep { Object getModel(BeforeDataT beforeData) throws Exception; } interface BeforeStep { BeforeDataT act() throws Exception; } interface AfterStep { void act(BeforeDataT beforeData) throws Exception; } void runBenchmark(BeforeStep beforeStep, AfterStep afterStep) throws Exception { runBenchmark( beforeStep, new LoadStep() { @Override public Object getModel(T beforeData) { return beforeData; } }, afterStep); } void runBenchmark(BeforeStep beforeStep, LoadStep loadStep, AfterStep afterStep) throws Exception { BenchmarkState state = benchmarkRule.getState(); Context app = ApplicationProvider.getApplicationContext(); while (state.keepRunning()) { state.pauseTiming(); Glide.get(app); T beforeData = beforeStep.act(); state.resumeTiming(); Glide.with(app) .load(loadStep.getModel(beforeData)) .diskCacheStrategy(DiskCacheStrategy.NONE) .override(Target.SIZE_ORIGINAL) .submit() .get(15, TimeUnit.SECONDS); state.pauseTiming(); tearDownGlide.tearDownGlide(); afterStep.act(beforeData); state.resumeTiming(); } } } ================================================ FILE: benchmark/src/androidTest/java/com/bumptech/glide/benchmark/data/DataOpener.java ================================================ package com.bumptech.glide.benchmark.data; import android.os.ParcelFileDescriptor; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.util.ByteBufferUtil; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; /** Converts test resources into various useful data types for benchmarking. */ public interface DataOpener { T acquire(@RawRes int resourceId) throws IOException; void close(T data) throws IOException; final class StreamOpener implements DataOpener { @Override public InputStream acquire(@RawRes int resourceId) { return ApplicationProvider.getApplicationContext().getResources().openRawResource(resourceId); } @Override public void close(InputStream data) throws IOException { data.close(); } } final class ByteArrayBufferOpener implements DataOpener { @Override public ByteBuffer acquire(@RawRes int resourceId) throws IOException { InputStream is = null; try { is = new StreamOpener().acquire(resourceId); return ByteBufferUtil.fromStream(is); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } } } @Override public void close(ByteBuffer data) {} } final class InputStreamOverByteArrayBufferOpener implements DataOpener { private final ByteArrayBufferOpener byteArrayBufferOpener = new ByteArrayBufferOpener(); @Nullable private ByteBuffer buffer; @Override public InputStream acquire(@RawRes int resourceId) throws IOException { buffer = byteArrayBufferOpener.acquire(resourceId); return ByteBufferUtil.toStream(buffer); } @Override public void close(InputStream data) throws IOException { data.close(); if (buffer != null) { byteArrayBufferOpener.close(buffer); } } } final class FileOpener implements DataOpener { @Override public File acquire(@RawRes int resourceId) throws IOException { ByteBuffer byteBuffer = new ByteArrayBufferOpener().acquire(resourceId); File tempFile = File.createTempFile( "memory_mapped", "jpg", ApplicationProvider.getApplicationContext().getCacheDir()); ByteBufferUtil.toFile(byteBuffer, tempFile); return tempFile; } @Override public void close(File data) { if (!data.delete()) { throw new IllegalStateException("Failed to delete: " + data); } } } final class ByteArrayOpener implements DataOpener { @Override public byte[] acquire(@RawRes int resourceId) throws IOException { InputStream is = null; try { is = new StreamOpener().acquire(resourceId); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024 * 1024]; int read; while ((read = is.read(buffer, /* off= */ 0, buffer.length)) != -1) { outputStream.write(buffer, /* off= */ 0, read); } return outputStream.toByteArray(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } } } @Override public void close(byte[] data) {} } final class ParcelFileDescriptorOpener implements DataOpener { private final FileOpener fileOpener = new FileOpener(); @Nullable private File file; @Override public ParcelFileDescriptor acquire(@RawRes int resourceId) throws IOException { file = fileOpener.acquire(resourceId); return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } @Override public void close(ParcelFileDescriptor data) throws IOException { data.close(); if (file != null) { fileOpener.close(file); } } } final class MemoryMappedByteBufferOpener implements DataOpener { private final FileOpener fileOpener = new FileOpener(); @Nullable private File file; @Override public ByteBuffer acquire(@RawRes int resourceId) throws IOException { file = fileOpener.acquire(resourceId); return ByteBufferUtil.fromFile(file); } @Override public void close(ByteBuffer data) { if (file != null) { fileOpener.close(file); } } } } ================================================ FILE: benchmark/src/androidTest/java/com/bumptech/glide/load/resource/bitmap/BenchmarkDownsampler.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.app.Application; import android.os.ParcelFileDescriptor; import androidx.benchmark.BenchmarkState; import androidx.benchmark.junit4.BenchmarkRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.benchmark.R; import com.bumptech.glide.benchmark.data.DataOpener; import com.bumptech.glide.benchmark.data.DataOpener.ByteArrayBufferOpener; import com.bumptech.glide.benchmark.data.DataOpener.ByteArrayOpener; import com.bumptech.glide.benchmark.data.DataOpener.FileOpener; import com.bumptech.glide.benchmark.data.DataOpener.InputStreamOverByteArrayBufferOpener; import com.bumptech.glide.benchmark.data.DataOpener.MemoryMappedByteBufferOpener; import com.bumptech.glide.benchmark.data.DataOpener.ParcelFileDescriptorOpener; import com.bumptech.glide.benchmark.data.DataOpener.StreamOpener; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; import com.bumptech.glide.request.target.Target; import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** Benchmarks to compare the performance of the various data types supported by Downsampler. */ @RunWith(AndroidJUnit4.class) public class BenchmarkDownsampler { private static final int SIZE = Target.SIZE_ORIGINAL; private static final int RESOURCE_ID = R.raw.pixel3a_portrait; private final Application app = ApplicationProvider.getApplicationContext(); @Rule public BenchmarkRule mBenchmarkRule = new BenchmarkRule(); @Test public void testInputStream() throws IOException { runBenchmark(new StreamOpener(), new InputStreamDecoder()); } @Test public void testByteBufferOverByteArray() throws IOException { runBenchmark(new ByteArrayBufferOpener(), new ByteBufferDecoder()); } @Test public void testByteBufferOverFile() throws IOException { runBenchmark(new MemoryMappedByteBufferOpener(), new ByteBufferDecoder()); } @Test public void testParcelFileDescriptorOverFile() throws IOException { runBenchmark(new ParcelFileDescriptorOpener(), new ParcelFileDescriptorDecoder()); } @Test public void testFile() throws IOException { runBenchmark(new FileOpener(), new FileDecoder()); } @Test public void testByteArray() throws IOException { runBenchmark(new ByteArrayOpener(), new ByteArrayDecoder()); } // A legacy case that's since been removed. @Test public void testInputStreamOverByteBufferOverByteArray() throws IOException { runBenchmark(new InputStreamOverByteArrayBufferOpener(), new InputStreamDecoder()); } private void runBenchmark(DataOpener opener, Decoder decoder) throws IOException { final BenchmarkState state = mBenchmarkRule.getState(); while (state.keepRunning()) { state.pauseTiming(); T data = null; try { data = opener.acquire(RESOURCE_ID); Downsampler downsampler = newDownsampler(); state.resumeTiming(); decoder.decode(downsampler, data, SIZE, SIZE); } finally { state.pauseTiming(); opener.close(data); state.resumeTiming(); } } } private interface Decoder { void decode(Downsampler downsampler, T data, int width, int height) throws IOException; } private static final class ByteBufferDecoder implements Decoder { @Override public void decode(Downsampler downsampler, ByteBuffer data, int width, int height) throws IOException { downsampler.decode(data, width, height, new Options()); } } private static final class InputStreamDecoder implements Decoder { @Override public void decode(Downsampler downsampler, InputStream data, int width, int height) throws IOException { downsampler.decode(data, width, height, new Options()); } } private static final class ParcelFileDescriptorDecoder implements Decoder { @Override public void decode(Downsampler downsampler, ParcelFileDescriptor data, int width, int height) throws IOException { downsampler.decode(data, width, height, new Options()); } } private static final class FileDecoder implements Decoder { @Override public void decode(Downsampler downsampler, File data, int width, int height) throws IOException { downsampler.decode(data, width, height, new Options()); } } private static final class ByteArrayDecoder implements Decoder { @Override public void decode(Downsampler downsampler, byte[] data, int width, int height) throws IOException { downsampler.decode(data, width, height, new Options()); } } private Downsampler newDownsampler() { ImmutableList imageHeaderParsers = ImmutableList.of(new DefaultImageHeaderParser(), new ExifInterfaceImageHeaderParser()); return new Downsampler( imageHeaderParsers, app.getResources().getDisplayMetrics(), new LruBitmapPool(20 * 1024 * 1024), new LruArrayPool(5 * 1024 * 1024)); } } ================================================ FILE: build.gradle ================================================ import org.gradle.api.tasks.testing.logging.TestExceptionFormat import se.bjurr.violations.gradle.plugin.ViolationsTask buildscript { repositories { google() mavenCentral() gradlePluginPortal() } dependencies { classpath libs.android.gradle if (!hasProperty('DISABLE_ERROR_PRONE')) { classpath libs.errorprone.gradle } classpath libs.proguard.gradle classpath libs.violations classpath libs.androidx.benchmark.gradle classpath libs.kotlin.gradle classpath libs.ksp.gradle.plugin classpath libs.coroutines.binarycompat.gradle classpath libs.dokka.gradle classpath libs.vanniktech classpath 'com.guardsquare:proguard-gradle:' + (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11) ? '7.3.2' : '7.1.0') } } plugins { alias libs.plugins.ktfmt } apply plugin: 'binary-compatibility-validator' apply plugin: 'org.jetbrains.dokka' apiValidation { ignoredProjects += ["ksp", "test", "gallery", "integrationtest", "sqljournaldiskcache"] nonPublicMarkers += ["com.bumptech.glide.integration.ktx.InternalGlideApi"] } // See http://blog.joda.org/2014/02/turning-off-doclint-in-jdk-8-javadoc.html. if (JavaVersion.current().isJava8Compatible()) { allprojects { tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') } } } dokkaHtmlMultiModule.configure { moduleName.set("Glide") } afterEvaluate { tasks.named("dokkaHtmlMultiModule") { pluginsMapConfiguration.set( [ "org.jetbrains.dokka.base.DokkaBase": """{ "customStyleSheets": ["${projectDir.toString()}/static/logo-styles.css"], "customAssets" : ["${projectDir.toString()}/static/logo-icon.svg", "${projectDir.toString()}/static/glide_circle_logo.png"] }""" ] ) } } subprojects { project -> // Exclude packages not intended for public use. if ([ "testutil", "flickr", "giphy", "imgur", "svg", "gallery", "contacturi", "test", "gif_decoder", "gifencoder", "compiler", "benchmark", "integrationtest", "instrumentation", "glide-parent", "integration", "samples", "third_party" ].contains(project.getName())) { afterEvaluate { project.apply plugin: 'org.jetbrains.dokka' project.tasks.dokkaHtmlPartial.enabled = false } } else { apply plugin: "com.ncorti.ktfmt.gradle" ktfmt { kotlinLangStyle() } afterEvaluate { project.apply plugin: 'org.jetbrains.dokka' project.tasks.dokkaHtmlPartial.configure { dokkaSourceSets { // Kotlin works out of the box if (!project.plugins.hasPlugin("kotlin-android") && project.plugins.hasPlugin("com.android.library")) { // Java Android modules register("main") { sourceRoots.from(project.android.sourceSets.main.java.srcDirs) } } else if (project.plugins.hasPlugin("java") && "ksp" != project.getName()) { // Java only modules (ksp is not useful and uses multiple plugins) register("main") { sourceRoots.from(sourceSets.main.java.srcDirs) } } } } } } afterEvaluate { if (project.plugins.hasPlugin("com.android.application")) { project.dependencies { // Hack around some version mismatches: https://stackoverflow.com/questions/75263047/duplicate-class-in-kotlin-android implementation(platform(libs.kotlin.bom)) } } } tasks.withType(JavaCompile) { // gifencoder is a legacy project that has a ton of warnings and is basically never // modified, so we're not going to worry about cleaning it up. // Imgur uses generated code from dagger that has warnings. if ("gifencoder" != project.getName() && "imgur" != project.getName()) { options.compilerArgs.addAll([ //Treat all warnings as errors. "-Werror", //Enable all warnings. "-Xlint:all", // Disable warnings about source 7 being obsolete. "-Xlint:-options", // Java expects every annotation to have a processor, but we use // javax.annotation.Nullable, which doesn't have one. "-Xlint:-processing", // See https://github.com/google/dagger/issues/945 // and https://bugs.openjdk.java.net/browse/JDK-8190452 "-Xlint:-classfile", // Disable deprecation warnings for ViewTarget/BaseTarget for now. "-Xlint:-deprecation", ]) } } tasks.withType(Test) { testLogging { exceptionFormat = TestExceptionFormat.FULL } } // Avoid issues like #2452. tasks.withType(Jar) { duplicatesStrategy = DuplicatesStrategy.FAIL } apply plugin: 'checkstyle' checkstyle { toolVersion = '8.45.1' } checkstyle { configFile = rootProject.file('checkstyle.xml') configProperties.checkStyleConfigDir = rootProject.rootDir } task checkstyle(type: Checkstyle) { source 'src' include '**/*.java' exclude '**/gen/**' // Caught by the violations plugin. ignoreFailures = true // empty classpath classpath = files() } apply plugin: "se.bjurr.violations.violations-gradle-plugin" task violations(type: ViolationsTask) { minSeverity = 'INFO' detailLevel = 'VERBOSE' maxViolations = 6 diffMaxViolations = 0 // Formats are listed here: https://github.com/tomasbjerre/violations-lib def dir = projectDir.absolutePath violations = [ ["PMD", dir, ".*/pmd/.*\\.xml\$", "PMD"], ["ANDROIDLINT", dir, ".*/lint-results\\.xml\$", "AndroidLint"], ["CHECKSTYLE", dir, ".*/checkstyle/.*\\.xml\$", "Checkstyle"], ] } afterEvaluate { if (project.hasProperty("android") && project.name != 'pmd' ) { android { lint { warningsAsErrors true quiet true // Caught by the violations plugin. abortOnError false } // We don't need a BuildConfig constants class. buildFeatures { buildConfig = false } // Signing isn't relevant to a library. Our signing is done via // a GPG key at upload time, there's no APK to sign. buildTypes { release { signingConfig signingConfigs.debug } } } } if (project.tasks.findByName('check')) { check.dependsOn('checkstyle') check.finalizedBy violations } } } ================================================ FILE: checkstyle.xml ================================================ ================================================ FILE: checkstyle_suppressions.xml ================================================ ================================================ FILE: glide/build.gradle ================================================ import com.android.build.gradle.api.LibraryVariant /** * This module is used for two things: *
    *
  • Compiling a single unified set of javadocs for Glide *
  • Providing a jar version of Glide for internal libraries, like * Glide's annotation processor. *
* *

Previously this module was used to produce a release jar for Glide, but * we've long since stopped releasing the jar. Now all release artifacts come * from the upload script, which uploads aars for each production submodule */ apply plugin: 'java' // The paths of Android projects that should be included only in Javadoc, not in the jar. static def getAndroidPathsForJavadoc() { [ ':integration:concurrent', ':integration:gifencoder', ':integration:okhttp', ':integration:okhttp3', ':integration:recyclerview', ':integration:volley', ':library', ':mocks', ':third_party:disklrucache', ':third_party:gif_decoder', ] } static def getAndroidPathsForJar() { [':library', ':third_party:disklrucache', ':third_party:gif_decoder'] } // The paths of Java projects that should be included only in Javadoc, not in the jar. static def getJavaPathsForJavadoc() { [':annotation'] } (getAndroidPathsForJavadoc() + getJavaPathsForJavadoc()).each { evaluationDependsOn(it) } def asProjects(paths) { paths.collect { String path -> project(path) } } def getAndroidSdkDirectory() { project(':library').android.sdkDirectory } def getAndroidCompileSdkVersion() { project(':library').android.compileSdkVersion } def getAndroidProjectsForJavadoc() { asProjects(getAndroidPathsForJavadoc()) } def getAndroidLibraryVariantsForJar() { getAndroidLibraryVariantsForProjects(asProjects(getAndroidPathsForJar())) } def getAndroidLibraryVariantsForJavadoc() { getAndroidLibraryVariantsForProjects(getAndroidProjectsForJavadoc()) } def getAndroidLibraryVariantsForProjects(projects) { projects.collect { project -> project.android.libraryVariants.findAll { type -> type.buildType.name.equalsIgnoreCase("release") } }.sum() } def getSourceFilesForJavadoc() { getAndroidProjectsForJavadoc().collect { project -> project.android.sourceSets.main.java.srcDirs } } def getAndroidJar() { "${getAndroidSdkDirectory()}/platforms/${getAndroidCompileSdkVersion()}/android.jar" } project.archivesBaseName = "${POM_ARTIFACT_ID}-${VERSION_NAME}" // Generate javadocs and sources containing batched documentation and sources for all internal // projects. def javadocTask = tasks.create("releaseJavadoc", Javadoc) { source = getSourceFilesForJavadoc() doFirst { it.classpath = project.files( getAndroidJar(), getAndroidLibraryVariantsForJavadoc().collect { LibraryVariant lib -> lib.getJavaCompileProvider().get().classpath.files }, // Finds dependencies of Android packages that would otherwise be // ignored (Volley in particular) getAndroidProjectsForJavadoc().collect { Project project -> project.file('build/intermediates/javac/release/classes') } ) } options { links("http://docs.oracle.com/javase/7/docs/api/") links("https://square.github.io/okhttp/3.x/okhttp/") links("https://square.github.io/okhttp/2.x/okhttp/") links("http://d.android.com/reference") } exclude '**/R.java' } def cleanJavadocTask = task("cleanReleaseJavadoc", type: Delete) { delete javadocTask.destinationDir } as Task clean.dependsOn(cleanJavadocTask) def javadocJarTask = task("releaseJavadocJar", type: Jar) { from javadocTask.destinationDir } as Task javadocJarTask.dependsOn(javadocTask) (getAndroidProjectsForJavadoc()).each { project -> releaseJavadoc.dependsOn(project.tasks.compileReleaseSources) jar.dependsOn(project.tasks.compileReleaseSources) } jar { from files( getAndroidLibraryVariantsForJar().collect { LibraryVariant variant -> variant.getJavaCompileProvider().get().destinationDirectory } ) exclude "**/R.class" exclude "**/R\$*.class" exclude "android/**" exclude "**/BuildConfig.class" } artifacts { archives releaseJavadocJar { archiveClassifier = 'javadoc' } } ================================================ FILE: glide/gradle.properties ================================================ POM_NAME=Glide Full POM_ARTIFACT_ID=glide-full POM_PACKAGING=jar ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] avif = "1.1.1.14d8e3c4" gson = "2.8.2" pmd = "6.0.0" dagger = "2.47" compose = "1.5.1" kotlin = "1.9.20" mockito = "5.3.1" retrofit = "2.3.0" coroutines = "1.8.0" ksp = "1.9.20-1.0.14" errorprone = "2.18.0" min-sdk-version = "14" target-sdk-version = "32" androidx-espresso = "3.5.1" androidx-fragment = "1.6.1" okhttp-min-sdk-version = "21" kotlin-compiler-extension = "1.5.5" androidx-benchmark = "1.2.0-beta05" compile-sdk-version = "android-36" androidx-multidex = "2.0.1" autoservice = "1.0-rc3" autoservice-annotations = "1.0.1" android-gradle = "8.1.1" androidx-cardview = "1.0.0" androidx-core = "1.12.0" androidx-annotation = "1.7.1" androidx-appcompat = "1.6.1" androidx-exifinterface = "1.3.6" androidx-futures = "1.1.0" androidx-junit = "1.1.5" androidx-lifecycle-runtime = "2.8.2" androidx-recyclerview = "1.3.1" androidx-test-core = "1.4.0" androidx-test-ktx = "1.5.0" androidx-test-rules = "1.4.0" androidx-test-runner = "1.4.0" androidx-tracing = "1.0.0" androidx-vectordrawable = "1.1.0" proguard-gradle = "7.1.0" coroutines-binarycompat-gradle = "0.18.1" cronet = "17.0.1" dokka-gradle = "1.8.20" drawablepainter = "0.25.1" errorprone-gradle = "2.0.2" findbugs-jsr305 = "3.0.2" guava = "28.1-android" guava-testlib = "18.0" javapoet = "1.9.0" junit = "4.13.2" kotlinpoet = "1.12.0" ksp-autoservice = "1.0.0" ksp-compiletesting = "1.6.0" mockwebserver = "3.0.0-RC1" okhttp2 = "2.7.5" okhttp3 = "3.10.0" okhttp4 = "4.10.0" robolectric = "4.11.1" rx-android = "1.2.1" rx-java = "1.3.8" svg = "1.2.1" truth = "1.4.4" violations = "1.8" volley = "1.2.1" vanniktech = "0.34.0" ktfmt = "0.25.0" [libraries] androidx-multidex = { group = "androidx.multidex", name = "multidex", version.ref = "androidx-multidex" } autoservice = { group = "com.google.auto.service", name = "auto-service", version.ref = "autoservice" } autoservice-annotations = { group = "com.google.auto.service", name = "auto-service-annotations", version.ref = "autoservice-annotations" } android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "android-gradle" } androidx-cardview = { group = "androidx.cardview", name = "cardview", version.ref = "androidx-cardview" } androidx-core = { group = "androidx.core", name = "core", version.ref = "androidx-core" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidx-annotation" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "androidx-exifinterface" } androidx-futures = { group = "androidx.concurrent", name = "concurrent-futures", version.ref = "androidx-futures" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime" } androidx-lifecycle-runtime-testing = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidx-lifecycle-runtime" } androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "androidx-recyclerview" } androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test-core" } androidx-test-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidx-test-ktx" } androidx-test-ktx-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidx-junit" } androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidx-test-rules" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } androidx-tracing = { group = "androidx.tracing", name = "tracing", version.ref = "androidx-tracing" } androidx-vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable-animated", version.ref = "androidx-vectordrawable" } avif = { module = "org.aomedia.avif.android:avif", version.ref = "avif" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } proguard-gradle = { group = "com.guardsquare", name = "proguard-gradle", version.ref = "proguard-gradle" } compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose" } coroutines-binarycompat-gradle = { group = "org.jetbrains.kotlinx", name = "binary-compatibility-validator", version.ref = "coroutines-binarycompat-gradle" } cronet = { group = "com.google.android.gms", name = "play-services-cronet", version.ref = "cronet" } dokka-gradle = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "dokka-gradle" } drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "drawablepainter" } errorprone-gradle = { group = "net.ltgt.gradle", name = "gradle-errorprone-plugin", version.ref = "errorprone-gradle" } findbugs-jsr305 = { group = "com.google.code.findbugs", name = "jsr305", version.ref = "findbugs-jsr305" } guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } guava-testlib = { group = "com.google.guava", name = "guava-testlib", version.ref = "guava-testlib" } javapoet = { group = "com.squareup", name = "javapoet", version.ref = "javapoet" } junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinpoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotlinpoet" } ksp-autoservice = { group = "dev.zacsweers.autoservice", name = "auto-service-ksp", version.ref = "ksp-autoservice" } ksp-compiletesting = { group = "com.github.tschuchortdev", name = "kotlin-compile-testing-ksp", version.ref = "ksp-compiletesting" } mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "mockwebserver" } okhttp2 = { group = "com.squareup.okhttp", name = "okhttp", version.ref = "okhttp2" } okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" } okhttp4 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp4" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } rx-android = { group = "io.reactivex", name = "rxandroid", version.ref = "rx-android" } rx-java = { group = "io.reactivex", name = "rxjava", version.ref = "rx-java" } svg = { group = "com.caverock", name = "androidsvg", version.ref = "svg" } truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } violations = { group = "se.bjurr.violations", name = "violations-gradle-plugin", version.ref = "violations" } volley = { group = "com.android.volley", name = "volley", version.ref = "volley" } vanniktech = { group = "com.vanniktech", name = "gradle-maven-publish-plugin", version.ref = "vanniktech" } androidx-benchmark-gradle = { group = "androidx.benchmark", name = "benchmark-gradle-plugin", version.ref = "androidx-benchmark" } androidx-benchmark-junit = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "androidx-benchmark" } androidx-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso" } androidx-espresso-idling = { group = "androidx.test.espresso.idling", name = "idling-concurrent", version.ref = "androidx-espresso" } androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidx-fragment" } androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidx-fragment" } compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "compose" } compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" } compose-ui-testmanifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" } compose-ui-testjunit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose" } coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } dagger-runtime = { group = "com.google.dagger", name = "dagger", version.ref = "dagger" } dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "dagger" } dagger-android = { group = "com.google.dagger", name = "dagger-android", version.ref = "dagger" } dagger-android-processor = { group = "com.google.dagger", name = "dagger-android-processor", version.ref = "dagger" } errorprone-annotations = { group = "com.google.errorprone", name = "error_prone_annotations", version.ref = "errorprone" } errorprone-core = { group = "com.google.errorprone", name = "error_prone_core", version.ref = "errorprone" } kotlin-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" } kotlin-jdk7 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk7", version.ref = "kotlin" } kotlin-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } kotlin-bom = { group = "org.jetbrains.kotlin", name = "kotlin-bom", version.ref = "kotlin" } ksp-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" } ksp-gradle-plugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } mockito-android = { group = "org.mockito", name = "mockito-android", version.ref = "mockito" } retrofit-runtime = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } retrofit-rxjava = { group = "com.squareup.retrofit2", name = "adapter-rxjava", version.ref = "retrofit" } [plugins] ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ ## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #Sun Jun 05 16:53:18 EST 2022 ## Grouping GROUP=com.github.bumptech.glide ## Metadata POM_DESCRIPTION=A fast and efficient image loading library for Android focused on smooth scrolling. POM_DEVELOPER_EMAIL=judds@google.com POM_DEVELOPER_ID=sjudd POM_DEVELOPER_NAME=Sam Judd POM_SCM_CONNECTION=scm\:git@github.com\:bumptech/glide.git POM_SCM_DEV_CONNECTION=scm\:git@github.com\:bumptech/glide.git POM_SCM_URL=https\://github.com/bumptech/glide POM_URL=https\://github.com/bumptech/glide ## Gradle config android.useAndroidX=true org.gradle.configureondemand=false org.gradle.daemon=true org.gradle.jvmargs=-Xmx4096M TEST_JVM_MEMORY_SIZE=4096M ## Glide versioning - these may be overwritten in lower level gradle.properties files VERSION_MAJOR=5 VERSION_MINOR=0 VERSION_PATCH=5 VERSION_NAME=5.0.5 ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: instrumentation/build.gradle.kts ================================================ tasks.configureEach { if (name == "lint") { enabled = false } } plugins { id("com.android.application") } android { namespace = "com.bumptech.glide.instrumentation" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } buildTypes { getByName("debug") { isDefault = true } } } dependencies { annotationProcessor(project(":annotation:compiler")) implementation(project(":library")) implementation(libs.androidx.multidex) implementation(libs.androidx.appcompat) androidTestImplementation(project(":library")) androidTestImplementation(project(":mocks")) androidTestImplementation(project(":testutil")) androidTestImplementation(libs.mockito.android) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.espresso.idling) androidTestImplementation(libs.androidx.espresso) androidTestImplementation(libs.truth) androidTestImplementation(libs.junit) androidTestImplementation(libs.androidx.exifinterface) androidTestImplementation(libs.findbugs.jsr305) } ================================================ FILE: instrumentation/gradle.properties ================================================ ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/AsBytesTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ModelGeneratorRule; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class AsBytesTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); @Rule public final ModelGeneratorRule modelGeneratorRule = new ModelGeneratorRule(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); } @Test public void loadImageResourceId_asBytes_providesBytesOfBitmap() { byte[] data = concurrency.get( Glide.with(context).as(byte[].class).load(ResourceIds.raw.canonical).submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadBitmap_asBytes_providesBytesOfBitmap() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); byte[] data = concurrency.get(Glide.with(context).as(byte[].class).load(bitmap).submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadBitmapDrawable_asBytes_providesBytesOfBitmap() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); byte[] data = concurrency.get( Glide.with(context) .as(byte[].class) .load(new BitmapDrawable(context.getResources(), bitmap)) .submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoResourceId_asBytes_providesBytesOfFrame() { byte[] data = concurrency.get(Glide.with(context).as(byte[].class).load(ResourceIds.raw.video).submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoResourceId_asBytes_withFrameTime_providesBytesOfFrame() { byte[] data = concurrency.get( GlideApp.with(context) .as(byte[].class) .load(ResourceIds.raw.video) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoFile_asBytes_providesByteOfFrame() throws IOException { byte[] data = concurrency.get(Glide.with(context).as(byte[].class).load(writeVideoToFile()).submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoFile_asBytes_withFrameTime_providesByteOfFrame() throws IOException { byte[] data = concurrency.get( GlideApp.with(context) .as(byte[].class) .load(writeVideoToFile()) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoFilePath_asBytes_providesByteOfFrame() throws IOException { byte[] data = concurrency.get( Glide.with(context) .as(byte[].class) .load(writeVideoToFile().getAbsolutePath()) .submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoFilePath_asBytes_withFrameTime_providesByteOfFrame() throws IOException { byte[] data = concurrency.get( GlideApp.with(context) .as(byte[].class) .load(writeVideoToFile().getAbsolutePath()) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoFileUri_asBytes_providesByteOfFrame() throws IOException { byte[] data = concurrency.get(Glide.with(context).as(byte[].class).load(writeVideoToFileUri()).submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } @Test public void loadVideoFileUri_asBytes_withFrameTime_providesByteOfFrame() throws IOException { byte[] data = concurrency.get( GlideApp.with(context) .as(byte[].class) .load(writeVideoToFileUri()) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(data).isNotNull(); assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } private File writeVideoToFile() throws IOException { return modelGeneratorRule.asFile(ResourceIds.raw.video); } private Uri writeVideoToFileUri() throws IOException { return Uri.fromFile(writeVideoToFile()); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/AsFileTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.content.Context; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ModelGeneratorRule; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.MockModelLoader; import com.bumptech.glide.testutil.TearDownGlide; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class AsFileTest { private static final String URL = "https://imgs.xkcd.com/comics/mc_hammer_age.png"; @Rule public final ModelGeneratorRule modelGeneratorRule = new ModelGeneratorRule(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private final Context context = ApplicationProvider.getApplicationContext(); @Before public void setUp() { MockModelLoader.mock(URL, getData()); } @Test public void asFile_withUrl_succeeds() { File file = concurrency.get(GlideApp.with(context).asFile().load(URL).submit()); assertThat(file).isNotNull(); } @Test public void asFile_withUrlAndDiskCacheStrategyAutomatic_succeeds() { File file = concurrency.get( GlideApp.with(context) .asFile() .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .load(URL) .submit()); assertThat(file).isNotNull(); } @Test public void asFile_withUrlAndDiskCacheStrategyData_succeeds() { File file = concurrency.get( GlideApp.with(context) .asFile() .diskCacheStrategy(DiskCacheStrategy.DATA) .load(URL) .submit()); assertThat(file).isNotNull(); } @Test public void asFile_withUrlAndDiskCacheStrategyResource_fails() { try { concurrency.get( GlideApp.with(context) .asFile() .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .load(URL) .submit()); fail(); } catch (RuntimeException e) { // expected. } } @Test public void asFile_withUrlAndDiskCacheStrategyAll_fails() { try { concurrency.get( GlideApp.with(context) .asFile() .diskCacheStrategy(DiskCacheStrategy.ALL) .load(URL) .submit()); fail(); } catch (RuntimeException e) { // Expected. } } private InputStream getData() { try { return new ByteArrayInputStream(modelGeneratorRule.asByteArray(ResourceIds.raw.canonical)); } catch (IOException e) { throw new RuntimeException(e); } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/CachingTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.testutil.BitmapSubject.assertThat; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.view.ViewGroup.LayoutParams; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.cache.LruResourceCache; import com.bumptech.glide.load.engine.cache.MemoryCacheAdapter; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.test.ResourceIds.raw; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.testutil.WaitModelLoader; import com.bumptech.glide.testutil.WaitModelLoader.WaitModel; import com.google.common.truth.Truth; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * Tests various aspects of memory and disk caching to verify resources can be retrieved as we * expect. */ @RunWith(AndroidJUnit4.class) public class CachingTest { private static final int IMAGE_SIZE_PIXELS = 500; // Store at least 10 500x500 pixel Bitmaps with the ARGB_8888 config to be safe. private static final long CACHE_SIZE_BYTES = IMAGE_SIZE_PIXELS * IMAGE_SIZE_PIXELS * 4 * 10; @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private RequestListener requestListener; private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); Glide.init(context, new GlideBuilder().setMemoryCache(new LruResourceCache(CACHE_SIZE_BYTES))); } @Test public void submit_withDisabledMemoryCache_andResourceInActiveResources_loadsFromMemory() { Glide.init(context, new GlideBuilder().setMemoryCache(new MemoryCacheAdapter())); FutureTarget first = GlideApp.with(context).load(raw.canonical).submit(); concurrency.get(first); concurrency.get( GlideApp.with(context).load(ResourceIds.raw.canonical).listener(requestListener).submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void submit_withRequestClearedFromMemory_doesNotLoadFromMemory() { Glide.init(context, new GlideBuilder().setMemoryCache(new MemoryCacheAdapter())); // Allow the request to be run and GCed without being cleared. concurrency.loadOnOtherThread( new Runnable() { @Override public void run() { FutureTarget first = GlideApp.with(context).load(raw.canonical).submit(); concurrency.get(first); } }); // Wait for the weak reference to be cleared and the request to be removed from active // resources. // De-flake by allowing multiple tries boolean isWeakRefCleared = false; for (int j = 0; j < 100; j++) { Runtime.getRuntime().gc(); concurrency.pokeMainThread(); try { // Loading again here won't shuffle our resource around because it only changes our // reference count from 1 to 2 and back. The reference we're waiting for will only be // decremented when the target is GCed. Target target = concurrency.wait( GlideApp.with(context) .load(ResourceIds.raw.canonical) .onlyRetrieveFromCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) .submit()); GlideApp.with(context).clear(target); } catch (RuntimeException e) { // The item has been cleared from active resources. isWeakRefCleared = true; break; } } if (!isWeakRefCleared) { fail("Failed to clear weak ref."); } concurrency.get( GlideApp.with(context).load(ResourceIds.raw.canonical).listener(requestListener).submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), not(eq(DataSource.MEMORY_CACHE)), anyBoolean()); } @Test public void submit_withPreviousRequestClearedFromMemory_completesFromDataDiskCache() { // Clearing the future here can race with clearing the EngineResource held on to by EngineJob // while it's notifying callbacks. Forcing all executors to use the same thread avoids the race // by making our clear and EngineJob's clear run on the same thread. GlideExecutor mainThreadExecutor = MockGlideExecutor.newMainThreadExecutor(); Glide.init( context, new GlideBuilder() .setSourceExecutor(mainThreadExecutor) .setDiskCacheExecutor(mainThreadExecutor) .setAnimationExecutor(mainThreadExecutor)); FutureTarget future = GlideApp.with(context) .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.DATA) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS); concurrency.get(future); GlideApp.with(context).clear(future); clearMemoryCacheOnMainThread(); concurrency.get( GlideApp.with(context) .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.DATA) .listener(requestListener) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS)); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.DATA_DISK_CACHE), anyBoolean()); } @Test public void submit_withPreviousButNoLongerReferencedIdenticalRequest_completesFromMemoryCache() { // We can't allow any mocks (RequestListner, Target etc) to reference this request or the test // will fail due to the transient strong reference to the request. concurrency.get( GlideApp.with(context) .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS)); // Force the collection of weak references now that the listener/request in the first load is no // longer referenced. Runtime.getRuntime().gc(); concurrency.pokeMainThread(); concurrency.get( GlideApp.with(context) .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .listener(requestListener) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS)); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void submit_withPreviousButNoLongerReferencedIdenticalRequest_doesNotRecycleBitmap() { // We can't allow any mocks (RequestListener, Target etc) to reference this request or the test // will fail due to the transient strong reference to the request. Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS)); // Force the collection of weak references now that the listener/request in the first load is no // longer referenced. Runtime.getRuntime().gc(); concurrency.pokeMainThread(); FutureTarget future = GlideApp.with(context) .asBitmap() .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS); concurrency.get(future); Glide.with(context).clear(future); clearMemoryCacheOnMainThread(); assertThat(bitmap).isNotRecycled(); } @Test public void clearDiskCache_doesNotPreventFutureLoads() { // Clearing the future here can race with clearing the EngineResource held on to by EngineJob // while it's notifying callbacks. Forcing all executors to use the same thread avoids the race // by making our clear and EngineJob's clear run on the same thread. GlideExecutor mainThreadExecutor = MockGlideExecutor.newMainThreadExecutor(); Glide.init( context, new GlideBuilder() .setSourceExecutor(mainThreadExecutor) .setDiskCacheExecutor(mainThreadExecutor) .setAnimationExecutor(mainThreadExecutor)); // Load the request once. FutureTarget future = GlideApp.with(context) .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.DATA) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS); concurrency.get(future); // Clear the result from all of our caches. GlideApp.with(context).clear(future); clearMemoryCacheOnMainThread(); GlideApp.get(context).clearDiskCache(); // Load the request a second time into the disk cache. future = GlideApp.with(context) .load(ResourceIds.raw.canonical) .diskCacheStrategy(DiskCacheStrategy.DATA) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS); concurrency.get(future); // Clear the second request from everywhere but the disk cache. GlideApp.with(context).clear(future); clearMemoryCacheOnMainThread(); // Load the request a third time. concurrency.get( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .diskCacheStrategy(DiskCacheStrategy.DATA) .submit(IMAGE_SIZE_PIXELS, IMAGE_SIZE_PIXELS)); // Assert that the third request comes from the disk cache (which was populated by the second // request). verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.DATA_DISK_CACHE), anyBoolean()); } // Tests #2428. @Test public void onlyRetrieveFromCache_withPreviousRequestLoadingFromSource_doesNotBlock() { final WaitModel waitModel = WaitModelLoader.waitOn(ResourceIds.raw.canonical); FutureTarget loadFromSourceFuture = GlideApp.with(context).load(waitModel).submit(); FutureTarget onlyFromCacheFuture = GlideApp.with(context).load(waitModel).onlyRetrieveFromCache(true).submit(); try { onlyFromCacheFuture.get(1000, TimeUnit.MILLISECONDS); fail("Expected only from cache Future to time out"); } catch (InterruptedException | TimeoutException e) { throw new RuntimeException(e); } catch (ExecutionException e) { // Expected. } waitModel.countDown(); Truth.assertThat(concurrency.get(loadFromSourceFuture)).isNotNull(); } // Tests #2428. @Test public void submit_withRequestLoadingWithOnlyRetrieveFromCache_andNotInCache_doesNotFail() { // Block the main thread so that we know that both requests will be queued but not started at // the same time. final CountDownLatch blockMainThread = new CountDownLatch(1); new Handler(Looper.getMainLooper()) .post( new Runnable() { @Override public void run() { try { blockMainThread.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); // Queue the retrieve from cache request first. final Future firstQueuedFuture = GlideApp.with(context).load(ResourceIds.raw.canonical).onlyRetrieveFromCache(true).submit(); // Then queue the normal request. FutureTarget expectedFuture = GlideApp.with(context).load(ResourceIds.raw.canonical).submit(); // Run the requests. blockMainThread.countDown(); // Verify that the request that didn't have retrieve from cache succeeds Truth.assertThat(concurrency.get(expectedFuture)).isNotNull(); // The first request only from cache should fail because the item is not in cache. assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() { concurrency.get(firstQueuedFuture); } }); } @Test public void loadIntoView_withoutSkipMemoryCache_loadsFromMemoryCacheIfPresent() { final ImageView imageView = new ImageView(context); imageView.setLayoutParams(new LayoutParams(100, 100)); concurrency.loadOnMainThread( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .dontTransform(), imageView); // Casting avoids a varags array warning. //noinspection rawtypes reset((RequestListener) requestListener); // Run on the main thread, since this is already cached, we shouldn't need to try to wait. If we // do end up re-using the old Target, our wait will always timeout anyway if we use // loadOnMainThread. If the load doesn't complete in time, it will be caught by the listener // below, which expects to be called synchronously. concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .dontTransform() .into(imageView); } }); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void loadIntoView_withSkipMemoryCacheFalse_loadsFromMemoryCacheIfPresent() { final ImageView imageView = new ImageView(context); imageView.setLayoutParams(new LayoutParams(100, 100)); concurrency.loadOnMainThread( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .skipMemoryCache(false) .dontTransform(), imageView); // Casting avoids a varags array warning. //noinspection rawtypes reset((RequestListener) requestListener); // Run on the main thread, since this is already cached, we shouldn't need to try to wait. If we // do end up re-using the old Target, our wait will always timeout anyway if we use // loadOnMainThread. If the load doesn't complete in time, it will be caught by the listener // below, which expects to be called synchronously. concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .skipMemoryCache(false) .dontTransform() .into(imageView); } }); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void loadIntoView_withSkipMemoryCache_doesNotLoadFromMemoryCacheIfPresent() { final ImageView imageView = new ImageView(context); imageView.setLayoutParams(new LayoutParams(100, 100)); concurrency.loadOnMainThread( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .dontTransform() .skipMemoryCache(true), imageView); // Casting avoids a varags array warning. //noinspection rawtypes reset((RequestListener) requestListener); // If this test fails due to a timeout, it's because we re-used the Target from the previous // request, which breaks the logic in loadOnMainThread that expects a new Target's // onResourceReady callback to be called. This can be confirmed by changing this to // runOnMainThread and verifying that the RequestListener assertion below fails because // the DataSource was from the memory cache. concurrency.loadOnMainThread( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .dontTransform() .skipMemoryCache(true), imageView); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), not(eq(DataSource.MEMORY_CACHE)), anyBoolean()); } private void clearMemoryCacheOnMainThread() { concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/CenterCropRegressionTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.BitmapRegressionTester; import com.bumptech.glide.test.CanonicalBitmap; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.RegressionTest; import com.bumptech.glide.test.SplitByCpu; import com.bumptech.glide.test.SplitBySdk; import com.bumptech.glide.testutil.TearDownGlide; import java.util.concurrent.ExecutionException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @RegressionTest @SplitByCpu @SplitBySdk({24, 21, 16}) public class CenterCropRegressionTest { @Rule public final TestName testName = new TestName(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private BitmapRegressionTester bitmapRegressionTester; private Context context; private CanonicalBitmap canonical; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); bitmapRegressionTester = BitmapRegressionTester.newInstance(getClass(), testName).assumeShouldRun(); canonical = new CanonicalBitmap(); } @Test public void centerCrop_withSquareSmallerThanImage_returnsSquareImage() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerCrop() .override(50)); assertThat(result.getWidth()).isEqualTo(50); assertThat(result.getHeight()).isEqualTo(50); } @Test public void centerCrop_withRectangleSmallerThanImage_returnsRectangularImage() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerCrop() .override(60, 70)); assertThat(result.getWidth()).isEqualTo(60); assertThat(result.getHeight()).isEqualTo(70); } @Test public void centerCrop_withSquareLargerThanImage_returnsUpscaledRectangularImage() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerCrop() .override(canonical.getWidth() * 2)); assertThat(result.getWidth()).isEqualTo(canonical.getWidth() * 2); assertThat(result.getHeight()).isEqualTo(canonical.getWidth() * 2); } @Test public void centerCrop_withRectangleLargerThanImage_returnsUpscaledRectangularImage() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerCrop() .override(canonical.getWidth() * 2, canonical.getHeight() * 2)); assertThat(result.getWidth()).isEqualTo(canonical.getWidth() * 2); assertThat(result.getHeight()).isEqualTo(canonical.getHeight() * 2); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/CenterInsideRegressionTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.BitmapRegressionTester; import com.bumptech.glide.test.CanonicalBitmap; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.RegressionTest; import com.bumptech.glide.test.SplitByCpu; import com.bumptech.glide.test.SplitBySdk; import com.bumptech.glide.testutil.TearDownGlide; import java.util.concurrent.ExecutionException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SplitByCpu @SplitBySdk({24, 21, 16}) @RegressionTest public class CenterInsideRegressionTest { @Rule public final TestName testName = new TestName(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private BitmapRegressionTester bitmapRegressionTester; private Context context; private CanonicalBitmap canonical; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); bitmapRegressionTester = BitmapRegressionTester.newInstance(getClass(), testName).assumeShouldRun(); canonical = new CanonicalBitmap(); } @Test public void centerInside_withSquareSmallerThanImage_returnsImageFitWithinSquare() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerInside() .override(50)); assertThat(result.getWidth()).isEqualTo(50); assertThat(result.getHeight()).isEqualTo(37); } @Test public void centerInside_withSquareLargerThanImage_returnsOriginalImage() throws ExecutionException, InterruptedException { float multiplier = 1.1f; int multipliedWidth = (int) (canonical.getWidth() * multiplier); Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerInside() .override(multipliedWidth)); assertThat(result.getWidth()).isEqualTo(canonical.getWidth()); assertThat(result.getHeight()).isEqualTo(canonical.getHeight()); } @Test public void centerInside_withNarrowRectangle_fitsWithinMaintainingAspectRatio() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerInside() .override(canonical.getWidth() / 10, canonical.getHeight())); assertThat(result.getWidth()).isEqualTo(canonical.getWidth() / 10); assertThat(result.getHeight()).isEqualTo(canonical.getHeight() / 10); } @Test public void centerInside_withShortRectangle_fitsWithinMaintainingAspectRatio() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .centerInside() .override(canonical.getWidth(), canonical.getHeight() / 2)); assertThat(result.getWidth()).isEqualTo(canonical.getWidth() / 2); assertThat(result.getHeight()).isEqualTo(canonical.getHeight() / 2); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/CircleCropRegressionTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.BitmapRegressionTester; import com.bumptech.glide.test.CanonicalBitmap; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.RegressionTest; import com.bumptech.glide.test.SplitByCpu; import com.bumptech.glide.test.SplitBySdk; import com.bumptech.glide.testutil.TearDownGlide; import java.util.concurrent.ExecutionException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SplitByCpu @SplitBySdk({26, 24, 23, 21, 18, 16}) @RegressionTest public class CircleCropRegressionTest { @Rule public final TestName testName = new TestName(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private BitmapRegressionTester bitmapRegressionTester; private Context context; private CanonicalBitmap canonical; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); bitmapRegressionTester = BitmapRegressionTester.newInstance(getClass(), testName).assumeShouldRun(); canonical = new CanonicalBitmap(); } @Test public void circleCrop_withSquareSmallerThanImage_returnsSquaredImage() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .circleCrop() .override(50)); assertThat(result.getWidth()).isEqualTo(50); assertThat(result.getHeight()).isEqualTo(50); } @Test public void circleCrop_withSquareLargerThanImage_returnsUpscaledFitImage() throws ExecutionException, InterruptedException { float multiplier = 1.1f; int multipliedWidth = (int) (canonical.getWidth() * multiplier); Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .circleCrop() .override(multipliedWidth)); assertThat(result.getWidth()).isEqualTo(multipliedWidth); assertThat(result.getHeight()).isEqualTo(multipliedWidth); } @Test public void circleCrop_withNarrowRectangle_cropsWithin() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .circleCrop() .override(canonical.getWidth() / 10, canonical.getHeight())); assertThat(result.getWidth()).isEqualTo(canonical.getWidth() / 10); assertThat(result.getHeight()).isEqualTo(canonical.getWidth() / 10); } @Test public void circleCrop_withShortRectangle_fitsWithinMaintainingAspectRatio() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .circleCrop() .override(canonical.getWidth(), canonical.getHeight() / 2)); assertThat(result.getWidth()).isEqualTo(canonical.getHeight() / 2); assertThat(result.getHeight()).isEqualTo(canonical.getHeight() / 2); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/DarkModeTest.java ================================================ package com.bumptech.glide; import static androidx.test.espresso.Espresso.onIdle; import static com.bumptech.glide.testutil.BitmapSubject.assertThat; import static org.junit.Assume.assumeTrue; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.instrumentation.R; import com.bumptech.glide.load.engine.executor.IdlingGlideRule; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.ForceDarkOrLightModeActivity; import com.google.common.base.Function; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DarkModeTest { private final Context context = ApplicationProvider.getApplicationContext(); @Rule public final IdlingGlideRule idlingGlideRule = IdlingGlideRule.newGlideRule(glideBuilder -> glideBuilder); @Before public void before() { // Dark mode wasn't supported prior to Q. assumeTrue(VERSION.SDK_INT >= VERSION_CODES.Q); } @Test public void load_withDarkModeActivity_vectorDrawable_usesDarkModeColor() { runActivityDrawableTest( darkModeActivity(), R.drawable.vector_drawable_dark, activity -> Glide.with(activity).load(R.drawable.vector_drawable).override(Target.SIZE_ORIGINAL)); } @Test public void load_withLightModeActivity_vectorDrawable_usesLightModeColor() { runActivityDrawableTest( lightModeActivity(), R.drawable.vector_drawable_light, activity -> Glide.with(activity).load(R.drawable.vector_drawable).override(Target.SIZE_ORIGINAL)); } private void runActivityDrawableTest( ActivityScenario scenario, int expectedResource, Function> glideBuilder) { AtomicReference result = new AtomicReference<>(); try (scenario) { scenario.onActivity( activity -> { ViewGroup container = findContainer(activity); ImageView imageView = newFixedSizeImageView(activity); container.addView(imageView); glideBuilder.apply(activity).into(imageView); }); // This two step process is because setting the Drawable on the ImageView modifies the // drawable in a subsequent frame. If we want our Drawables to produce identical Bitmaps when // drawn to a canvas, we need to set both on the ImageView for at least one frame. onIdle(); scenario.onActivity( activity -> { ImageView imageView = findImageView(activity); result.set(drawableToBitmap(imageView.getDrawable())); Drawable expectedDrawable = AppCompatResources.getDrawable(activity, expectedResource); imageView.setImageDrawable(expectedDrawable); }); onIdle(); scenario.onActivity( activity -> { ImageView imageView = findImageView(activity); Bitmap expected = drawableToBitmap(imageView.getDrawable()); assertThat(result.get()).sameAs(expected); }); } } private static Bitmap drawableToBitmap(Drawable drawable) { int width = drawable.getIntrinsicWidth(); int height = drawable.getIntrinsicHeight(); Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(result); drawable.setBounds(0, 0, width, height); drawable.draw(canvas); canvas.setBitmap(null); return result; } @Test public void load_withDarkModeActivity_useDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL)); } @Test public void load_withDarkModeActivity_afterLoadingWithLightModeActivity_useDarkModeDrawable() { // Load with light mode first. runActivityTest( lightModeActivity(), R.raw.dog_light, activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL)); // Then again with dark mode to make sure that we do not use the cached resource from the // previous load. runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL)); } @Test public void load_withDarkModeActivity_afterLoadingWithLightModeActivity_memoryCacheCleared_useDarkModeDrawable() { // Load with light mode first. runActivityTest( lightModeActivity(), R.raw.dog_light, activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL)); // Then again with dark mode to make sure that we do not use the cached resource from the // previous load. runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> { Glide.get(context).clearMemory(); return Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL); }); } @Test public void load_withDarkModeFragment_usesDarkModeDrawable() { runFragmentTest( darkModeActivity(), R.raw.dog_dark, fragment -> Glide.with(fragment).load(R.drawable.dog).override(Target.SIZE_ORIGINAL)); } @Test public void load_withLightModeActivity_usesLightModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL)); } @Test public void load_withLightModeFragment_usesLightModeDrawable() { runFragmentTest( lightModeActivity(), R.raw.dog_light, fragment -> Glide.with(fragment).load(R.drawable.dog).override(Target.SIZE_ORIGINAL)); } @Test public void load_withDarkModeActivity_darkModeTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity) .load(R.drawable.dog) .override(Target.SIZE_ORIGINAL) .theme(activity.getTheme())); } @Test public void loadResourceNameUri_withDarkModeActivity_darkModeTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity) .load(newResourceNameUri(activity, R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(activity.getTheme())); } @Test public void loadResourceNameUri_withDarkModeActivity_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity) .load(newResourceNameUri(activity, R.drawable.dog)) .override(Target.SIZE_ORIGINAL)); } @Test public void loadResourceNameUri_withDarkModeActivity_afterLightModeActivity_usesDarkModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, activity -> Glide.with(activity) .load(newResourceNameUri(activity, R.drawable.dog)) .override(Target.SIZE_ORIGINAL)); runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity) .load(newResourceNameUri(activity, R.drawable.dog)) .override(Target.SIZE_ORIGINAL)); } @Test public void loadResourceIdUri_withDarkModeActivity_darkModeTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity) .load(newResourceIdUri(activity, R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(activity.getTheme())); } @Test public void loadResourceIdUri_withDarkModeActivity_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, activity -> Glide.with(activity) .load(newResourceIdUri(activity, R.drawable.dog)) .override(Target.SIZE_ORIGINAL)); } private static Uri newResourceNameUri(Context context, int resourceId) { Resources resources = context.getResources(); return newResourceUriBuilder(context) .appendPath(resources.getResourceTypeName(resourceId)) .appendPath(resources.getResourceEntryName(resourceId)) .build(); } private static Uri newResourceIdUri(Context context, int resourceId) { return newResourceUriBuilder(context).appendPath(String.valueOf(resourceId)).build(); } private static Uri.Builder newResourceUriBuilder(Context context) { return new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()); } @Test public void load_withDarkModeFragment_darkModeTheme_usesDarkModeDrawable() { runFragmentTest( darkModeActivity(), R.raw.dog_dark, fragment -> Glide.with(fragment) .load(R.drawable.dog) .override(Target.SIZE_ORIGINAL) .theme(fragment.requireActivity().getTheme())); } @Test public void loadResourceNameUri_withDarkModeFragment_darkModeTheme_usesDarkModeDrawable() { runFragmentTest( darkModeActivity(), R.raw.dog_dark, fragment -> Glide.with(fragment) .load(newResourceNameUri(fragment.requireContext(), R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(fragment.requireActivity().getTheme())); } @Test public void loadResourceIdUri_withDarkModeFragment_darkModeTheme_usesDarkModeDrawable() { runFragmentTest( darkModeActivity(), R.raw.dog_dark, fragment -> Glide.with(fragment) .load(newResourceIdUri(fragment.requireContext(), R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(fragment.requireActivity().getTheme())); } @Test public void load_withApplicationContext_darkTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input.getApplicationContext()) .load(R.drawable.dog) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); } @Ignore("TODO(#3751): Consider how to deal with themes applied for application context loads.") @Test public void load_withApplicationContext_lightTheme_thenDarkTheme_usesDarkModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, input -> Glide.with(input.getApplicationContext()) .load(R.drawable.dog) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input.getApplicationContext()) .load(R.drawable.dog) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); } @Test public void loadResourceNameUri_withApplicationContext_darkTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input.getApplicationContext()) .load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); } @Ignore("TODO(#3751): Consider how to deal with themes applied for application context loads.") @Test public void loadResourceNameUri_withApplicationContext_darkTheme_afterLightTheme_usesDarkModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, input -> Glide.with(input.getApplicationContext()) .load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input.getApplicationContext()) .load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); } @Test public void loadResourceIdUri_withApplicationContext_darkTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input.getApplicationContext()) .load(newResourceIdUri(input.getApplicationContext(), R.drawable.dog)) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); } @Test public void load_withApplicationContext_lightTheme_usesLightModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, input -> Glide.with(input.getApplicationContext()) .load(R.drawable.dog) .override(Target.SIZE_ORIGINAL) .theme(input.getTheme())); } @Test public void load_withLightModeActivity_lightModeTheme_usesLightModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, activity -> Glide.with(activity) .load(R.drawable.dog) .override(Target.SIZE_ORIGINAL) .theme(activity.getTheme())); } @Test public void placeholder_withDarkModeActivity_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog)); } @Test public void placeholder_withDarkModeFragment_usesDarkModeDrawable() { runFragmentTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog)); } @Test public void error_withDarkModeActivity_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input).load((Object) null).error(R.drawable.dog)); } @Test public void error_withDarkModeFragment_usesDarkModeDrawable() { runFragmentTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input).load((Object) null).error(R.drawable.dog)); } @Test public void fallback_withDarkModeActivity_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input).load((Object) null).fallback(R.drawable.dog)); } @Test public void fallback_withDarkModeFragment_usesDarkModeDrawable() { runFragmentTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input).load((Object) null).fallback(R.drawable.dog)); } @Test public void placeholder_withLightModeActivity_usesLightModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog)); } @Test public void placeholder_withLightModeFragment_usesLightModeDrawable() { runFragmentTest( lightModeActivity(), R.raw.dog_light, input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog)); } @Test public void placeholder_withDarkModeActivityAndTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input) .load((Object) null) .theme(input.getTheme()) .placeholder(R.drawable.dog)); } @Test public void placeholder_withLightModeActivityAndTheme_usesLightModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, input -> Glide.with(input) .load((Object) null) .theme(input.getTheme()) .placeholder(R.drawable.dog)); } @Test public void placeholder_withApplicationContext_darkTheme_usesDarkModeDrawable() { runActivityTest( darkModeActivity(), R.raw.dog_dark, input -> Glide.with(input.getApplicationContext()) .load((Object) null) .theme(input.getTheme()) .placeholder(R.drawable.dog)); } @Test public void placeholder_withApplicationContext_lightTheme_usesLightModeDrawable() { runActivityTest( lightModeActivity(), R.raw.dog_light, input -> Glide.with(input.getApplicationContext()) .load((Object) null) .theme(input.getTheme()) .placeholder(R.drawable.dog)); } private ActivityScenario darkModeActivity() { return ActivityScenario.launch(ForceDarkOrLightModeActivity.forceDarkMode(context)); } private ActivityScenario lightModeActivity() { return ActivityScenario.launch(ForceDarkOrLightModeActivity.forceLightMode(context)); } private static void runFragmentTest( ActivityScenario scenario, int expectedResource, Function> requestBuilder) { try (scenario) { scenario.onActivity( activity -> { ImageViewFragment fragment = new ImageViewFragment(); activity .getSupportFragmentManager() .beginTransaction() .add(R.id.container, fragment) .commitNowAllowingStateLoss(); ViewGroup container = findContainer(activity); ImageView imageView = (ImageView) container.getChildAt(0); requestBuilder.apply(fragment).into(imageView); }); assertImageViewContainerChildHasContent(scenario, expectedResource); } } /** Fragment that displays a single fixed size ImageView. */ public static final class ImageViewFragment extends Fragment { @Override public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return newFixedSizeImageView(getContext()); } } private static ImageView newFixedSizeImageView(Context context) { ImageView imageView = new ImageView(context); imageView.setLayoutParams(new LayoutParams(200, 200)); return imageView; } private static void runActivityTest( ActivityScenario scenario, int expectedResource, Function> glideBuilder) { try (scenario) { scenario.onActivity( activity -> { ViewGroup container = findContainer(activity); ImageView imageView = newFixedSizeImageView(activity); container.addView(imageView); glideBuilder.apply(activity).into(imageView); }); assertImageViewContainerChildHasContent(scenario, expectedResource); } } private static void assertImageViewContainerChildHasContent( ActivityScenario scenario, int expectedResource) { onIdle(); scenario.onActivity( activity -> { ImageView imageView = findImageView(activity); Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); assertThat(bitmap).sameAs(expectedResource); }); } private static ImageView findImageView(FragmentActivity activity) { ViewGroup container = findContainer(activity); return (ImageView) container.getChildAt(0); } private static ViewGroup findContainer(FragmentActivity activity) { return activity.findViewById(R.id.container); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/DataUriTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.Base64; import androidx.core.content.ContextCompat; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.util.Preconditions; import java.io.ByteArrayOutputStream; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DataUriTest { @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private final Context context = ApplicationProvider.getApplicationContext(); @Test public void load_withJpegAsDataUriString_returnsBitmap() { Bitmap bitmap = concurrency.get( Glide.with(context).asBitmap().load(getDataUriString(CompressFormat.JPEG)).submit()); assertThat(bitmap).isNotNull(); } @Test public void load_withPngDataUriString_returnsBitmap() { Bitmap bitmap = concurrency.get( Glide.with(context).asBitmap().load(getDataUriString(CompressFormat.PNG)).submit()); assertThat(bitmap).isNotNull(); } @Test public void load_withJpegAsDataUri_returnsBitmap() { Bitmap bitmap = concurrency.get( Glide.with(context).asBitmap().load(getDataUri(CompressFormat.JPEG)).submit()); assertThat(bitmap).isNotNull(); } @Test public void load_withPngAsDataUri_returnsBitmap() { Bitmap bitmap = concurrency.get( Glide.with(context).asBitmap().load(getDataUri(CompressFormat.PNG)).submit()); assertThat(bitmap).isNotNull(); } private Uri getDataUri(CompressFormat format) { return Uri.parse(getDataUriString(format)); } private String getDataUriString(CompressFormat format) { String bytes = getBase64BitmapBytes(format); String imageType; switch (format) { case PNG: imageType = "png"; break; case JPEG: imageType = "jpeg"; break; case WEBP: imageType = "webp"; break; default: throw new IllegalArgumentException("Unrecognized format: " + format); } String mimeType = "image/" + imageType; return "data:" + mimeType + ";base64," + bytes; } @SuppressWarnings("deprecation") private String getBase64BitmapBytes(CompressFormat format) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); Drawable drawable = Preconditions.checkNotNull(ContextCompat.getDrawable(context, ResourceIds.raw.canonical)); Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); bitmap.compress(format, 100, bos); byte[] data = bos.toByteArray(); return Base64.encodeToString(data, /* flags= */ 0); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/DownsampleVideoTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.testutil.BitmapSubject.assertThat; import static org.junit.Assume.assumeTrue; import android.content.Context; import android.graphics.Bitmap; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DownsampleVideoTest { // The dimensions of the test video. private static final int WIDTH = 1080; private static final int HEIGHT = 1920; @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private final Context context = ApplicationProvider.getApplicationContext(); @Before public void setUp() { assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1); } @Test public void loadVideo_downsampleStrategyNone_returnsOriginalVideoDimensions() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .load(ResourceIds.raw.video) .downsample(DownsampleStrategy.NONE) .submit(10, 10)); assertThat(bitmap).hasDimensions(WIDTH, HEIGHT); } @Test public void loadVideo_downsampleStrategyNone_doesNotUpscale() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .load(ResourceIds.raw.video) .downsample(DownsampleStrategy.NONE) .submit(WIDTH * 2, HEIGHT * 2)); assertThat(bitmap).hasDimensions(WIDTH, HEIGHT); } @Test public void loadVideo_downsampleDefault_downsamplesVideo() { Bitmap bitmap = concurrency.get( GlideApp.with(context).asBitmap().load(ResourceIds.raw.video).submit(10, 10)); assertThat(bitmap).hasDimensions(10, 18); } @Test public void loadVideo_downsampleAtMost_downsamplesToSmallerSize() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.AT_MOST) .load(ResourceIds.raw.video) .submit(540, 959)); assertThat(bitmap).hasDimensions(270, 480); } @Test public void loadVideo_downsampleAtMost_doesNotUpscale() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.AT_MOST) .load(ResourceIds.raw.video) .submit(WIDTH * 2, HEIGHT * 2)); assertThat(bitmap).hasDimensions(WIDTH, HEIGHT); } @Test public void loadVideo_downsampleAtLeast_downsamplesToLargerSize() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.AT_LEAST) .load(ResourceIds.raw.video) .submit(270, 481)); assertThat(bitmap).hasDimensions(540, 960); } @Test public void loadVideo_downsampleAtLeast_doesNotUpscale() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.AT_LEAST) .load(ResourceIds.raw.video) .submit(WIDTH * 2, HEIGHT * 2)); assertThat(bitmap).hasDimensions(WIDTH, HEIGHT); } @Test public void loadVideo_downsampleCenterInside_downsamplesWithinBox() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.CENTER_INSIDE) .load(ResourceIds.raw.video) .submit(270, 481)); assertThat(bitmap).hasDimensions(270, 480); } @Test public void loadVideo_downsampleCenterInside_doesNotUpscale() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.CENTER_INSIDE) .load(ResourceIds.raw.video) .submit(WIDTH * 2, HEIGHT * 2)); assertThat(bitmap).hasDimensions(WIDTH, HEIGHT); } @Test public void loadVideo_downsampleCenterOutside_downsamplesOutsideBox() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.CENTER_OUTSIDE) .load(ResourceIds.raw.video) .submit(270, 481)); assertThat(bitmap).hasDimensions(271, 481); } @Test public void loadVideo_downsampleCenterOutside_upsacles() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.CENTER_OUTSIDE) .load(ResourceIds.raw.video) .submit(WIDTH * 2, HEIGHT * 2)); assertThat(bitmap).hasDimensions(WIDTH * 2, HEIGHT * 2); } @Test public void loadVideo_downsampleFitCenter_downsamplesInsideBox() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.FIT_CENTER) .load(ResourceIds.raw.video) .submit(270, 481)); assertThat(bitmap).hasDimensions(270, 480); } @Test public void loadVideo_downsampleFitCenter_upscales() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.FIT_CENTER) .load(ResourceIds.raw.video) .submit(WIDTH * 2, HEIGHT * 2)); assertThat(bitmap).hasDimensions(WIDTH * 2, HEIGHT * 2); } @Test public void loadVideo_withSizeOriginal_ignoresDownsampleStrategy() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .downsample(DownsampleStrategy.AT_MOST) .load(ResourceIds.raw.video) .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)); assertThat(bitmap).hasDimensions(WIDTH, HEIGHT); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/DrawableTransformationTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.testutil.BitmapSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.TransformationUtils; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.test.GlideApp; import com.google.common.truth.Truth; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DrawableTransformationTest { private Context context; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); } @After public void tearDown() { Glide.get(context).clearDiskCache(); Glide.tearDown(); } @Test public void load_withColorDrawable_sizeOriginal_optionalTransform_returnsColorDrawable() throws ExecutionException, InterruptedException { Drawable colorDrawable = new ColorDrawable(Color.RED); Drawable result = Glide.with(context) .load(colorDrawable) .apply(new RequestOptions().optionalCenterCrop()) .submit() .get(); Truth.assertThat(result).isInstanceOf(ColorDrawable.class); assertThat(((ColorDrawable) result).getColor()).isEqualTo(Color.RED); } /** Transformations that do nothing can simply return the original Bitmap. */ @Test public void load_withColorDrawable_fixedSize_requiredUnitTransform_returnsOriginalDrawable() throws ExecutionException, InterruptedException { Drawable colorDrawable = new ColorDrawable(Color.RED); Drawable result = Glide.with(context) .load(colorDrawable) .apply(new RequestOptions().centerCrop()) .submit(100, 100) .get(); Truth.assertThat(result).isInstanceOf(ColorDrawable.class); assertThat(((ColorDrawable) result).getColor()).isEqualTo(Color.RED); } /** * Transformations that produce a different output color/shape/image etc will end up returning a * {@link Bitmap} based on the original {@link Drawable} but with the transformation applied. */ @Test public void load_withColorDrawable_fixedSize_nonUnitRequiredTransform_returnsBitmapDrawable() throws ExecutionException, InterruptedException { Drawable colorDrawable = new ColorDrawable(Color.RED); Drawable result = Glide.with(context) .load(colorDrawable) .apply(new RequestOptions().circleCrop()) .submit(100, 100) .get(); Bitmap redSquare = Bitmap.createBitmap(100, 100, Config.ARGB_8888); Canvas canvas = new Canvas(redSquare); canvas.drawColor(Color.RED); BitmapPool bitmapPool = mock(BitmapPool.class); when(bitmapPool.get(100, 100, Bitmap.Config.ARGB_8888)) .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); Bitmap expected = TransformationUtils.circleCrop(bitmapPool, redSquare, 100, 100); Truth.assertThat(result).isInstanceOf(BitmapDrawable.class); Bitmap bitmap = ((BitmapDrawable) result).getBitmap(); assertThat(bitmap.getWidth()).isEqualTo(100); assertThat(bitmap.getHeight()).isEqualTo(100); for (int x = 0; x < bitmap.getWidth(); x++) { for (int y = 0; y < bitmap.getHeight(); y++) { assertThat(bitmap.getPixel(x, y)).isEqualTo(expected.getPixel(x, y)); } } } @Test public void load_withColorDrawable_sizeOriginal_requiredTransform_fails() throws ExecutionException, InterruptedException { final Drawable colorDrawable = new ColorDrawable(Color.RED); // The following section is a hack to workaround a weird behavior where a post in RequestManager // can cause a failed request to be started twice in a row if the first attempt happens before. // the post. This seems rather unlikely to happen in real applications and it only occurs when // the request fails unexpectedly, so we're working around this weird behavior in this test. // See #3551. // Trigger the Glide application RequestManager to be created. Glide.get(context).getRequestManagerRetriever().get(context); // Wait until it's added as a lifecycle observer. final CountDownLatch latch = new CountDownLatch(1); new Handler(Looper.getMainLooper()) .post( new Runnable() { @Override public void run() { latch.countDown(); } }); latch.await(5, TimeUnit.SECONDS); // End hacks. assertThrows( ExecutionException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { Glide.with(context) .load(colorDrawable) .apply(new RequestOptions().centerCrop()) .submit() .get(); } }); } @Test public void load_withBitmapDrawable_andDoNothingTransformation_doesNotRecycleBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Bitmap.createBitmap(100, 200, Config.ARGB_8888); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); Drawable result = GlideApp.with(context) .load(drawable) .fitCenter() .override(bitmap.getWidth(), bitmap.getHeight()) .submit() .get(); assertThat(result).isNotRecycled(); } @Test public void load_withBitmapDrawable_andFunctionalTransformation_doesNotRecycleBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Bitmap.createBitmap(100, 200, Config.ARGB_8888); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); Drawable result = GlideApp.with(context) .load(drawable) .fitCenter() .override(bitmap.getWidth() / 2, bitmap.getHeight() / 2) .submit() .get(); assertThat(result).isNotRecycled(); } @Test public void load_withColorDrawable_fixedSize_unitBitmapTransform_recyclesIntermediates() throws ExecutionException, InterruptedException { Drawable colorDrawable = new ColorDrawable(Color.RED); int width = 100; int height = 200; GlideApp.with(context).load(colorDrawable).fitCenter().override(width, height).submit().get(); BitmapPool bitmapPool = Glide.get(context).getBitmapPool(); // Make sure we didn't put the same Bitmap twice. Bitmap first = bitmapPool.get(width, height, Config.ARGB_8888); Bitmap second = bitmapPool.get(width, height, Config.ARGB_8888); assertThat(first).isNotSameInstanceAs(second); } @Test public void load_withColorDrawable_fixedSize_functionalBitmapTransform_doesNotRecycleOutput() throws ExecutionException, InterruptedException { Drawable colorDrawable = new ColorDrawable(Color.RED); int width = 100; int height = 200; Drawable result = GlideApp.with(context) .load(colorDrawable) .circleCrop() .override(width, height) .submit() .get(); assertThat(result).isNotRecycled(); BitmapPool bitmapPool = Glide.get(context).getBitmapPool(); // Make sure we didn't put the same Bitmap twice. Bitmap first = bitmapPool.get(width, height, Config.ARGB_8888); Bitmap second = bitmapPool.get(width, height, Config.ARGB_8888); assertThat(first).isNotSameInstanceAs(second); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/ErrorHandlingTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.GlideExecutor.UncaughtThrowableStrategy; import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.testutil.WaitModelLoader; import com.bumptech.glide.testutil.WaitModelLoader.WaitModel; import java.io.File; import java.util.concurrent.CountDownLatch; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class ErrorHandlingTest { @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private RequestListener requestListener; private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); } private WaitForErrorStrategy initializeGlideWithWaitForErrorStrategy() { WaitForErrorStrategy strategy = new WaitForErrorStrategy(); Glide.init( context, new GlideBuilder() .setAnimationExecutor( GlideExecutor.newAnimationExecutor(/* threadCount= */ 1, strategy)) .setSourceExecutor(GlideExecutor.newSourceExecutor(strategy)) .setDiskCacheExecutor(GlideExecutor.newDiskCacheExecutor(strategy))); Glide.get(context) .getRegistry() .prepend(Bitmap.class, new FailEncoder()) .prepend( BitmapDrawable.class, new BitmapDrawableEncoder(Glide.get(context).getBitmapPool(), new FailEncoder())); return strategy; } // ResourceEncoders are expected not to throw and to return true or false. If they do throw, it's // a developer error, so we expect UncaughtThrowableStrategy to be called. @Test public void load_whenEncoderFails_callsUncaughtThrowableStrategy() { WaitForErrorStrategy strategy = initializeGlideWithWaitForErrorStrategy(); concurrency.get( Glide.with(context).load(ResourceIds.raw.canonical).listener(requestListener).submit()); // Writing to the disk cache and therefore the exception caused by our FailEncoder may happen // after the request completes, so we should wait for the expected error explicitly. ConcurrencyHelper.waitOnLatch(strategy.latch); assertThat(strategy.error).isEqualTo(FailEncoder.TO_THROW); verify(requestListener, never()) .onLoadFailed( any(GlideException.class), any(), ArgumentMatchers.>any(), anyBoolean()); } @Test public void load_whenLoadSucceeds_butEncoderFails_doesNotCallOnLoadFailed() { WaitForErrorStrategy strategy = initializeGlideWithWaitForErrorStrategy(); concurrency.get( Glide.with(context).load(ResourceIds.raw.canonical).listener(requestListener).submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), any(DataSource.class), anyBoolean()); verify(requestListener, never()) .onLoadFailed( any(GlideException.class), any(), ArgumentMatchers.>any(), anyBoolean()); } @Test public void clearRequest_withError_afterPrimaryFails_clearsErrorRequest() { WaitModel errorModel = WaitModelLoader.waitOn(ResourceIds.raw.canonical); FutureTarget target = Glide.with(context) .load((Object) null) .error(Glide.with(context).load(errorModel).listener(requestListener)) .submit(); Glide.with(context).clear(target); errorModel.countDown(); // Make sure any pending requests run. concurrency.pokeMainThread(); Glide.tearDown(); // Make sure that any callbacks posted back to the main thread run. concurrency.pokeMainThread(); } private static final class WaitForErrorStrategy implements UncaughtThrowableStrategy { final CountDownLatch latch = new CountDownLatch(1); @Nullable Throwable error = null; @Override public void handle(Throwable t) { if (error != null) { throw new IllegalArgumentException("Received second error", t); } error = t; latch.countDown(); } } private static final class FailEncoder implements ResourceEncoder { static final RuntimeException TO_THROW = new RuntimeException(); @NonNull @Override public EncodeStrategy getEncodeStrategy(@NonNull Options options) { return EncodeStrategy.TRANSFORMED; } @Override public boolean encode( @NonNull Resource data, @NonNull File file, @NonNull Options options) { throw TO_THROW; } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/ExternallyClearedDiskCacheTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.DiskCache.Factory; import com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.test.ResourceIds.raw; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.File; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; // Tests #2465. @RunWith(AndroidJUnit4.class) public class ExternallyClearedDiskCacheTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; private File cacheDir; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); cacheDir = context.getCacheDir(); } @After public void tearDown() { // Force us to wait until Glide's threads shut down. Glide.tearDown(); deleteRecursively(cacheDir); } @Test public void clearDiskCache_afterOpeningDiskCache_andDeleteDirectoryOutsideGlide_doesNotThrow() { DiskCache cache = DiskLruCacheWrapper.create(cacheDir, 1024 * 1024); cache.get(mock(Key.class)); deleteRecursively(cacheDir); cache.clear(); } @Test public void get_afterDeleteDirectoryOutsideGlideAndClose_doesNotThrow() { DiskCache cache = DiskLruCacheWrapper.create(cacheDir, 1024 * 1024); cache.get(mock(Key.class)); deleteRecursively(cacheDir); cache.clear(); cache.get(mock(Key.class)); } @Test public void loadFromCache_afterDiskCacheDeletedAndCleared_doesNotFail() { final DiskCache cache = DiskLruCacheWrapper.create(cacheDir, 1024 * 1024); cache.get(mock(Key.class)); deleteRecursively(cacheDir); cache.clear(); Glide.init( context, new GlideBuilder() .setDiskCache( new Factory() { @Override public DiskCache build() { return cache; } })); Drawable drawable = concurrency.get(Glide.with(context).load(ResourceIds.raw.canonical).submit()); assertThat(drawable).isNotNull(); } @Test public void loadFromCache_afterDiskCacheDeleted_doesNotFail() { final DiskCache cache = DiskLruCacheWrapper.create(cacheDir, 1024 * 1024); cache.get(mock(Key.class)); deleteRecursively(cacheDir); Glide.init( context, new GlideBuilder() .setDiskCache( new Factory() { @Override public DiskCache build() { return cache; } })); Drawable drawable = concurrency.get(Glide.with(context).load(raw.canonical).submit()); assertThat(drawable).isNotNull(); } private static void deleteRecursively(File file) { if (file.isDirectory()) { File[] files = file.listFiles(); if (files != null) { for (File f : files) { deleteRecursively(f); } } } if (!file.delete() && file.exists()) { throw new RuntimeException("Failed to delete: " + file); } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/FitCenterRegressionTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.BitmapRegressionTester; import com.bumptech.glide.test.CanonicalBitmap; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.RegressionTest; import com.bumptech.glide.test.SplitByCpu; import com.bumptech.glide.test.SplitBySdk; import com.bumptech.glide.testutil.TearDownGlide; import java.util.concurrent.ExecutionException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.rules.TestName; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SplitByCpu @SplitBySdk({24, 23, 21, 19, 18, 16}) @RegressionTest public class FitCenterRegressionTest { @Rule public final TestName testName = new TestName(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private BitmapRegressionTester bitmapRegressionTester; private Context context; private CanonicalBitmap canonical; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); bitmapRegressionTester = BitmapRegressionTester.newInstance(getClass(), testName).assumeShouldRun(); canonical = new CanonicalBitmap(); } @Test public void fitCenter_withSquareSmallerThanImage_returnsImageFitWithinSquare() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context).asBitmap().load(canonical.getBitmap()).fitCenter().override(50)); assertThat(result.getWidth()).isEqualTo(50); assertThat(result.getHeight()).isEqualTo(37); } @Test public void fitCenter_withSquareLargerThanImage_returnsUpscaledSquare() throws ExecutionException, InterruptedException { float multiplier = 1.1f; int multipliedWidth = (int) (canonical.getWidth() * multiplier); int multipliedHeight = (int) (canonical.getHeight() * multiplier); Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .fitCenter() .override(multipliedWidth)); assertThat(result.getWidth()).isEqualTo(multipliedWidth); assertThat(result.getHeight()).isEqualTo(multipliedHeight); } @Test public void fitCenter_withNarrowRectangle_fitsWithinMaintainingAspectRatio() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .fitCenter() .override(canonical.getWidth() / 10, canonical.getHeight())); assertThat(result.getWidth()).isEqualTo(canonical.getWidth() / 10); assertThat(result.getHeight()).isEqualTo(canonical.getHeight() / 10); } @Test public void fitCenter_withShortRectangle_fitsWithinMaintainingAspectRatio() throws ExecutionException, InterruptedException { Bitmap result = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .fitCenter() .override(canonical.getWidth(), canonical.getHeight() / 2)); assertThat(result.getWidth()).isEqualTo(canonical.getWidth() / 2); assertThat(result.getHeight()).isEqualTo(canonical.getHeight() / 2); } @Test public void fitCenter_withHugeRectangle_throwsOOM() throws ExecutionException, InterruptedException { float multiplier = Integer.MAX_VALUE / (canonical.getWidth() * canonical.getHeight() * 2); final int overrideWidth = (int) multiplier * canonical.getWidth(); final int overrideHeight = (int) multiplier * canonical.getHeight(); assertThrows( ExecutionException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { GlideApp.with(context) .asBitmap() .load(canonical.getBitmap()) .fitCenter() .override(overrideWidth, overrideHeight) .submit() .get(); } }); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LargeImageTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.model.UnitModelLoader; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Objects; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class LargeImageTest { @Rule public final TestRule tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private final Context context = ApplicationProvider.getApplicationContext(); @Test public void loadLargeJpeg_asByteArray_succeeds() throws IOException { byte[] data = getLargeImageBytes(); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadLargeJpeg_asByteBuffer_succeeds() throws IOException { // Using UnitModelLoader lets us mimic loading the ByteBuffer from some other data type, which // reduces the scope of our test. Glide.get(context) .getRegistry() .append( ByteBuffer.class, ByteBuffer.class, UnitModelLoader.Factory.getInstance()); ByteBuffer buffer = ByteBuffer.wrap(getLargeImageBytes()); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(buffer).submit()); assertThat(bitmap).isNotNull(); } private byte[] getLargeImageBytes() throws IOException { Resources resources = context.getResources(); int resourceId = ResourceIds.raw.canonical_large; Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(resources.getResourcePackageName(resourceId)) .appendPath(resources.getResourceTypeName(resourceId)) .appendPath(resources.getResourceEntryName(resourceId)) .build(); InputStream is = Objects.requireNonNull(context.getContentResolver().openInputStream(uri)); ByteArrayOutputStream os = new ByteArrayOutputStream(); byte[] buffer = new byte[1024 * 1024 * 5]; int read; while ((read = is.read(buffer, 0, buffer.length)) != -1) { os.write(buffer, 0, read); } return os.toByteArray(); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LoadAnimatedImageResourceTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; import android.content.ContentResolver; import android.content.Context; import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.IOException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; /** * Tests that Glide is able to load animated images (WebP and AVIF) stored in resources and loaded * as {@link android.graphics.drawable.AnimatedImageDrawable}s when the underlying Android platform * supports it. */ @RunWith(AndroidJUnit4.class) public class LoadAnimatedImageResourceTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; private static final boolean IS_ANIMATED_WEBP_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; private static final boolean IS_ANIMATED_AVIF_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); } @Test public void loadAnimatedImageResourceId_fromInt_decodesAnimatedImageDrawable_Webp() { assumeTrue(IS_ANIMATED_WEBP_SUPPORTED); Drawable frame = concurrency.get(Glide.with(context).load(ResourceIds.raw.animated_webp).submit()); assertThat(frame).isNotNull(); assertThat(frame).isInstanceOf(AnimatedImageDrawable.class); } @Test public void loadAnimatedImageResourceId_fromInt_decodesAnimatedImageDrawable_Avif() { assumeTrue(IS_ANIMATED_AVIF_SUPPORTED); Drawable frame = concurrency.get(Glide.with(context).load(ResourceIds.raw.animated_avif).submit()); assertThat(frame).isNotNull(); assertThat(frame).isInstanceOf(AnimatedImageDrawable.class); } @Test public void loadAnimatedImageUri_fromId_decodesAnimatedImageDrawable_Webp() { assumeTrue(IS_ANIMATED_WEBP_SUPPORTED); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path(String.valueOf(ResourceIds.raw.animated_webp)) .build(); Drawable frame = concurrency.get(GlideApp.with(context).load(uri).submit()); assertThat(frame).isNotNull(); assertThat(frame).isInstanceOf(AnimatedImageDrawable.class); } @Test public void loadAnimatedImageUri_fromId_decodesAnimatedImageDrawable_Avif() { assumeTrue(IS_ANIMATED_AVIF_SUPPORTED); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path(String.valueOf(ResourceIds.raw.animated_avif)) .build(); Drawable frame = concurrency.get(GlideApp.with(context).load(uri).submit()); assertThat(frame).isNotNull(); assertThat(frame).isInstanceOf(AnimatedImageDrawable.class); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LoadAssetUriTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.IOException; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; /** * Tests that Glide is able to load images and videos stored in assets and loaded as {@link * android.content.res.AssetFileDescriptor}s. */ @RunWith(AndroidJUnit4.class) public class LoadAssetUriTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private static final String VIDEO_ASSET_NAME = "video.mp4"; private static final String IMAGE_ASSET_NAME = "canonical.jpg"; private Context context; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); } @Test public void loadVideoAssetUri_decodesFrame() { Uri uri = Uri.parse(assetNameToUri(VIDEO_ASSET_NAME)); Drawable frame = concurrency.get(GlideApp.with(context).load(uri).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoAssetUri_asBitmap_decodesFrame() { Uri uri = Uri.parse(assetNameToUri(VIDEO_ASSET_NAME)); Bitmap frame = concurrency.get(GlideApp.with(context).asBitmap().load(uri).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoAssetUri_withFrame_decodesFrame() { Uri uri = Uri.parse(assetNameToUri(VIDEO_ASSET_NAME)); Bitmap frame = concurrency.get( GlideApp.with(context) .asBitmap() .load(uri) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoAssetUriString_decodesFrame() { Uri uri = Uri.parse(assetNameToUri(VIDEO_ASSET_NAME)); Bitmap frame = concurrency.get(GlideApp.with(context).asBitmap().load(uri.toString()).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoAssetUriString_withFrame_decodesFrame() { Uri uri = Uri.parse(assetNameToUri(VIDEO_ASSET_NAME)); Bitmap frame = concurrency.get( GlideApp.with(context) .asBitmap() .load(uri.toString()) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } @Test public void loadImageAssetUri_decodesImage() { Uri uri = Uri.parse(assetNameToUri(IMAGE_ASSET_NAME)); Drawable image = concurrency.get(GlideApp.with(context).load(uri).submit()); assertThat(image).isNotNull(); } @Test public void loadImageAssetUri_asBitmap_decodesImage() { Uri uri = Uri.parse(assetNameToUri(IMAGE_ASSET_NAME)); Bitmap image = concurrency.get(GlideApp.with(context).asBitmap().load(uri).submit()); assertThat(image).isNotNull(); } @Test public void loadImageAssetUriString_decodesImage() { Uri uri = Uri.parse(assetNameToUri(IMAGE_ASSET_NAME)); Bitmap image = concurrency.get(GlideApp.with(context).asBitmap().load(uri.toString()).submit()); assertThat(image).isNotNull(); } private static String assetNameToUri(String assetName) { return "file:///android_asset/" + assetName; } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LoadBitmapTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; import com.bumptech.glide.load.engine.cache.LruResourceCache; import com.bumptech.glide.load.engine.cache.MemoryCacheAdapter; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.util.Util; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class LoadBitmapTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private RequestListener bitmapListener; @Mock private RequestListener drawableListener; private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; private GlideBuilder glideBuilder; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); // Clearing the future here can race with clearing the EngineResource held on to by EngineJob // while it's notifying callbacks. Forcing all executors to use the same thread avoids the race // by making our clear and EngineJob's clear run on the same thread. GlideExecutor mainThreadExecutor = MockGlideExecutor.newMainThreadExecutor(); glideBuilder = new GlideBuilder() .setSourceExecutor(mainThreadExecutor) .setDiskCacheExecutor(mainThreadExecutor) .setAnimationExecutor(mainThreadExecutor); } @Test public void clearFromRequestBuilder_asDrawable_withLoadedBitmap_doesNotRecycleBitmap() { Glide.init( context, new GlideBuilder() .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Target target = concurrency.wait(GlideApp.with(context).asDrawable().load(bitmap).submit(100, 100)); Glide.with(context).clear(target); // Allow Glide's resource recycler to run on the main thread. concurrency.pokeMainThread(); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void transformFromRequestBuilder_asDrawable_withLoadedBitmap_doesNotRecycleBitmap() { Glide.init( context, new GlideBuilder() .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); concurrency.wait( GlideApp.with(context).asDrawable().load(bitmap).centerCrop().submit(100, 100)); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void clearFromRequestManager_withLoadedBitmap_doesNotRecycleBitmap() { Glide.init( context, new GlideBuilder() .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Target target = concurrency.wait(GlideApp.with(context).load(bitmap).submit(100, 100)); Glide.with(context).clear(target); // Allow Glide's resource recycler to run on the main thread. concurrency.pokeMainThread(); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void transformFromRequestManager_withLoadedBitmap_doesNotRecycleBitmap() { Glide.init( context, new GlideBuilder() .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); concurrency.wait(GlideApp.with(context).load(bitmap).centerCrop().submit(100, 100)); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void clearFromRequestBuilder_withLoadedBitmap_asBitmap_doesNotRecycleBitmap() { Glide.init( context, new GlideBuilder() .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Target target = concurrency.wait(GlideApp.with(context).asBitmap().load(bitmap).submit(100, 100)); Glide.with(context).clear(target); // Allow Glide's resource recycler to run on the main thread. concurrency.pokeMainThread(); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void transformFromRequestBuilder_withLoadedBitmap_asBitmap_doesNotRecycleBitmap() { Glide.init( context, new GlideBuilder() .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); concurrency.wait(GlideApp.with(context).asBitmap().load(bitmap).centerCrop().submit(100, 100)); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void loadFromRequestManager_withBitmap_doesNotLoadFromDiskCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait(GlideApp.with(context).load(bitmap).centerCrop().submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .load(bitmap) .centerCrop() .listener(drawableListener) .submit(100, 100)); verify(drawableListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_asDrawable_withBitmap_doesNotLoadFromDiskCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait( GlideApp.with(context).asDrawable().load(bitmap).centerCrop().submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .load(bitmap) .centerCrop() .listener(drawableListener) .submit(100, 100)); verify(drawableListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_asDrawable_withBitmapAndStrategyBeforeLoad_notFromCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait( GlideApp.with(context) .asDrawable() .diskCacheStrategy(DiskCacheStrategy.ALL) .load(bitmap) .centerCrop() .submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .load(bitmap) .centerCrop() .listener(drawableListener) .submit(100, 100)); verify(drawableListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_asBitmap_withBitmap_doesNotLoadFromDiskCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait( GlideApp.with(context).asBitmap().load(bitmap).centerCrop().submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .asBitmap() .load(bitmap) .centerCrop() .listener(bitmapListener) .submit(100, 100)); verify(bitmapListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_asBitmap_withBitmapAndStrategyBeforeLoad_notFromCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait( GlideApp.with(context) .asBitmap() .diskCacheStrategy(DiskCacheStrategy.ALL) .load(bitmap) .centerCrop() .submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .asBitmap() .load(bitmap) .centerCrop() .listener(bitmapListener) .submit(100, 100)); verify(bitmapListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LoadBytesTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.test.GlideOptions.skipMemoryCacheOf; import static com.bumptech.glide.testutil.BitmapSubject.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.widget.AbsListView.LayoutParams; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.google.common.io.ByteStreams; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class LoadBytesTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); @Mock private RequestListener requestListener; private Context context; private ImageView imageView; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); imageView = new ImageView(context); int[] dimensions = getCanonicalDimensions(); imageView.setLayoutParams(new LayoutParams(/* w= */ dimensions[0], /* h= */ dimensions[1])); // Writes to the resource disk cache run in a non-blocking manner after the Target is notified. // Unless we enforce a single threaded executor, the encode task races with our second decode // task, causing the test to sometimes fail (when the second resource is started after the // encode and loaded from the disk cache) and sometimes succeed (when the second resource is // started before the encode and loads from source). ExecutorService executor = Executors.newSingleThreadExecutor(); GlideExecutor glideExecutor = MockGlideExecutor.newTestExecutor(executor); Glide.init( context, new GlideBuilder() .setAnimationExecutor(glideExecutor) .setDiskCacheExecutor(glideExecutor) .setSourceExecutor(glideExecutor)); } @Test public void loadFromRequestManager_intoImageView_withDifferentByteArrays_loadsDifferentImages() throws IOException { final byte[] canonicalBytes = getCanonicalBytes(); final byte[] modifiedBytes = getModifiedBytes(); concurrency.loadOnMainThread(Glide.with(context).load(canonicalBytes), imageView); Bitmap firstBitmap = copyFromImageViewDrawable(imageView); concurrency.loadOnMainThread(Glide.with(context).load(modifiedBytes), imageView); Bitmap secondBitmap = copyFromImageViewDrawable(imageView); // This assertion alone doesn't catch the case where the second Bitmap is loaded from the result // cache of the data from the first Bitmap. assertThat(firstBitmap).isNotSameInstanceAs(secondBitmap); Bitmap expectedCanonicalBitmap = BitmapFactory.decodeByteArray(canonicalBytes, /* offset= */ 0, canonicalBytes.length); assertThat(firstBitmap).sameAs(expectedCanonicalBitmap); Bitmap expectedModifiedBitmap = BitmapFactory.decodeByteArray(modifiedBytes, /* offset= */ 0, modifiedBytes.length); assertThat(secondBitmap).sameAs(expectedModifiedBitmap); } @Test public void loadFromRequestBuilder_intoImageView_withDifferentByteArrays_loadsDifferentImages() throws IOException { final byte[] canonicalBytes = getCanonicalBytes(); final byte[] modifiedBytes = getModifiedBytes(); concurrency.loadOnMainThread( GlideApp.with(context).asDrawable().load(canonicalBytes), imageView); Bitmap firstBitmap = copyFromImageViewDrawable(imageView); concurrency.loadOnMainThread( GlideApp.with(context).asDrawable().load(modifiedBytes), imageView); Bitmap secondBitmap = copyFromImageViewDrawable(imageView); // This assertion alone doesn't catch the case where the second Bitmap is loaded from the result // cache of the data from the first Bitmap. assertThat(firstBitmap).isNotSameInstanceAs(secondBitmap); Bitmap expectedCanonicalBitmap = BitmapFactory.decodeByteArray(canonicalBytes, /* offset= */ 0, canonicalBytes.length); assertThat(firstBitmap).sameAs(expectedCanonicalBitmap); Bitmap expectedModifiedBitmap = BitmapFactory.decodeByteArray(modifiedBytes, /* offset= */ 0, modifiedBytes.length); assertThat(secondBitmap).sameAs(expectedModifiedBitmap); } @Test public void requestManager_intoImageView_withSameByteArrayAndMemoryCacheEnabled_loadsFromMemory() throws IOException { final byte[] canonicalBytes = getCanonicalBytes(); concurrency.loadOnMainThread( Glide.with(context).load(canonicalBytes).apply(skipMemoryCacheOf(false)), imageView); Glide.with(context).clear(imageView); concurrency.loadOnMainThread( Glide.with(context) .load(canonicalBytes) .listener(requestListener) .apply(skipMemoryCacheOf(false)), imageView); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void requestBuilder_intoImageView_withSameByteArrayAndMemoryCacheEnabled_loadsFromMemory() throws IOException { final byte[] canonicalBytes = getCanonicalBytes(); concurrency.loadOnMainThread( Glide.with(context).asDrawable().load(canonicalBytes).apply(skipMemoryCacheOf(false)), imageView); Glide.with(context).clear(imageView); concurrency.loadOnMainThread( Glide.with(context) .asDrawable() .load(canonicalBytes) .listener(requestListener) .apply(skipMemoryCacheOf(false)), imageView); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void loadFromRequestManager_withSameByteArray_validDiskCacheStrategy_returnsFromDiskCache() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait( GlideApp.with(context) .load(data) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .submit()); GlideApp.with(context).clear(target); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .load(data) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .listener(requestListener) .submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.RESOURCE_DISK_CACHE), anyBoolean()); } @Test public void loadFromRequestBuilder_withSameByteArray_validDiskCacheStrategy_returnsFromDiskCache() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait( GlideApp.with(context) .asDrawable() .load(data) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .submit()); GlideApp.with(context).clear(target); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .asDrawable() .load(data) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .listener(requestListener) .submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.RESOURCE_DISK_CACHE), anyBoolean()); } @Test public void loadFromRequestManager_withSameByteArray_memoryCacheEnabled_returnsFromCache() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait(GlideApp.with(context).load(data).skipMemoryCache(false).submit()); GlideApp.with(context).clear(target); concurrency.wait( GlideApp.with(context) .load(data) .skipMemoryCache(false) .listener(requestListener) .submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void loadFromRequestBuilder_withSameByteArray_memoryCacheEnabled_returnsFromCache() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait( GlideApp.with(context).asDrawable().load(data).skipMemoryCache(false).submit()); GlideApp.with(context).clear(target); concurrency.wait( GlideApp.with(context) .asDrawable() .load(data) .skipMemoryCache(false) .listener(requestListener) .submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void loadFromRequestManager_withSameByteArray_returnsFromLocal() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait(GlideApp.with(context).load(data).submit()); GlideApp.with(context).clear(target); concurrency.wait(GlideApp.with(context).load(data).listener(requestListener).submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_withSameByteArray_returnsFromLocal() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait(GlideApp.with(context).asDrawable().load(data).submit()); GlideApp.with(context).clear(target); concurrency.wait( GlideApp.with(context).asDrawable().load(data).listener(requestListener).submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestManager_withSameByteArrayAndMissingFromMemory_returnsFromLocal() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait(GlideApp.with(context).load(data).submit()); GlideApp.with(context).clear(target); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.get(context).clearMemory(); } }); concurrency.wait(GlideApp.with(context).load(data).listener(requestListener).submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_withSameByteArrayAndMissingFromMemory_returnsFromLocal() throws IOException { byte[] data = getCanonicalBytes(); Target target = concurrency.wait(GlideApp.with(context).asDrawable().load(data).submit()); GlideApp.with(context).clear(target); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context).asDrawable().load(data).listener(requestListener).submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromBuilder_withDiskCacheStrategySetBeforeLoad_doesNotOverrideDiskCacheStrategy() throws IOException { byte[] data = getCanonicalBytes(); concurrency.wait( GlideApp.with(context) .asDrawable() .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .load(data) .submit()); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .asDrawable() .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .listener(requestListener) .load(data) .submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.RESOURCE_DISK_CACHE), anyBoolean()); } @Test public void loadFromBuilder_withSkipMemoryCacheSetBeforeLoad_doesNotOverrideSkipMemoryCache() throws IOException { byte[] data = getCanonicalBytes(); concurrency.wait( GlideApp.with(context).asDrawable().skipMemoryCache(false).load(data).submit()); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context) .asDrawable() .skipMemoryCache(false) .listener(requestListener) .load(data) .submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void loadFromBuilder_withDataDiskCacheStrategy_returnsFromSource() throws IOException { byte[] data = getCanonicalBytes(); concurrency.wait( GlideApp.with(context) .asDrawable() .diskCacheStrategy(DiskCacheStrategy.DATA) .load(data) .submit()); concurrency.wait( GlideApp.with(context) .asDrawable() .diskCacheStrategy(DiskCacheStrategy.DATA) .skipMemoryCache(true) .load(data) .listener(requestListener) .submit()); verify(requestListener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.DATA_DISK_CACHE), anyBoolean()); } private Bitmap copyFromImageViewDrawable(ImageView imageView) { if (imageView.getDrawable() == null) { fail("Drawable unexpectedly null"); } // Glide mutates Bitmaps, so it's possible that a Bitmap loaded into a View in one place may // be re-used to load a different image later. Create a defensive copy just in case. return Bitmap.createBitmap(((BitmapDrawable) imageView.getDrawable()).getBitmap()); } private int[] getCanonicalDimensions() throws IOException { byte[] canonicalBytes = getCanonicalBytes(); Bitmap bitmap = BitmapFactory.decodeByteArray(canonicalBytes, /* offset= */ 0, canonicalBytes.length); return new int[] {bitmap.getWidth(), bitmap.getHeight()}; } private byte[] getModifiedBytes() throws IOException { int[] dimensions = getCanonicalDimensions(); Bitmap bitmap = Bitmap.createBitmap(dimensions[0], dimensions[1], Config.ARGB_8888); ByteArrayOutputStream os = new ByteArrayOutputStream(); bitmap.compress(CompressFormat.PNG, /* quality= */ 100, os); return os.toByteArray(); } private byte[] getCanonicalBytes() throws IOException { int resourceId = ResourceIds.raw.canonical; Resources resources = context.getResources(); InputStream is = resources.openRawResource(resourceId); return ByteStreams.toByteArray(is); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LoadDrawableTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; import com.bumptech.glide.load.engine.cache.LruResourceCache; import com.bumptech.glide.load.engine.cache.MemoryCacheAdapter; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.util.Util; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class LoadDrawableTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); @Mock private RequestListener listener; private Context context; private GlideExecutor executor; private GlideBuilder glideBuilder; @Before public void setUp() { MockitoAnnotations.initMocks(this); executor = MockGlideExecutor.newMainThreadExecutor(); context = ApplicationProvider.getApplicationContext(); glideBuilder = new GlideBuilder() .setAnimationExecutor(executor) .setSourceExecutor(executor) .setDiskCacheExecutor(executor); } @Test public void clear_withLoadedBitmapDrawable_doesNotRecycleBitmap() { Glide.init( context, glideBuilder .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); Target target = concurrency.wait(GlideApp.with(context).load(drawable).submit(100, 100)); Glide.with(context).clear(target); // Allow Glide's resource recycler to run on the main thread. concurrency.pokeMainThread(); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void transform_withLoadedBitmapDrawable_doesNotRecycleBitmap() { Glide.init( context, glideBuilder .setMemoryCache(new MemoryCacheAdapter()) .setBitmapPool(new BitmapPoolAdapter())); Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); concurrency.wait(GlideApp.with(context).load(drawable).centerCrop().submit(100, 100)); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void loadFromRequestManager_withBitmap_doesNotLoadFromDiskCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait(GlideApp.with(context).load(drawable).centerCrop().submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context).load(drawable).centerCrop().listener(listener).submit(100, 100)); verify(listener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_asDrawable_withBitmap_doesNotLoadFromDiskCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait( GlideApp.with(context).asDrawable().load(drawable).centerCrop().submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context).load(drawable).centerCrop().listener(listener).submit(100, 100)); verify(listener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void loadFromRequestBuilder_asDrawable_withBitmapAndStrategyBeforeLoad_notFromCache() { Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), ResourceIds.raw.canonical); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); Glide.init( context, glideBuilder .setMemoryCache(new LruResourceCache(Util.getBitmapByteSize(bitmap) * 10)) .setBitmapPool(new LruBitmapPool(Util.getBitmapByteSize(bitmap) * 10))); Target target = concurrency.wait( GlideApp.with(context) .asDrawable() .diskCacheStrategy(DiskCacheStrategy.ALL) .load(drawable) .centerCrop() .submit(100, 100)); Glide.with(context).clear(target); assertThat(bitmap.isRecycled()).isFalse(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.get(context).clearMemory(); } }); concurrency.wait( GlideApp.with(context).load(drawable).centerCrop().listener(listener).submit(100, 100)); verify(listener) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.LOCAL), anyBoolean()); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LoadResourcesWithDownsamplerTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ColorSpace; import android.net.Uri; import android.os.Build; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.util.Util; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** * On API 26, decoding a variety of different images can cause {@link BitmapFactory} with {@link * BitmapFactory.Options#inJustDecodeBounds} set to {@code true} to set {@link * BitmapFactory.Options#outConfig} to null instead of a valid value, even though the image can be * decoded successfully. Glide can mask these failures by decoding some image sources (notably * including resource ids) using other data types and decoders. * *

This test ensures that we've worked around the framework issue by loading a variety of images * and image types without the normal fallback behavior. */ @RunWith(AndroidJUnit4.class) public class LoadResourcesWithDownsamplerTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private final Context context = ApplicationProvider.getApplicationContext(); @Test public void loadJpegResource_withNoOtherLoaders_decodesResource() { Glide.get(context) .getRegistry() .prepend(Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.canonical)); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadWideGamutJpegResource_withNoOtherLoaders_decodesWideGamutBitmap() { assumeTrue( "Wide gamut is only available on O+", Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); Glide.get(context) .getRegistry() .prepend( Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.webkit_logo_p3)); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit()); assertThat(bitmap).isNotNull(); assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.RGBA_F16); // The exact value here depends on the emulator / device we're running on. On Pixel devices and // emulators it'll return DISPLAY_P3. On 'generic' emulators and some other devices, it'll // return LINEAR_EXTENDED_SRGB. It's unclear how else we can assert correctly based on the // device type, so I've just left this is isAnyOf for now. assertThat(bitmap.getColorSpace()) .isAnyOf( ColorSpace.get(ColorSpace.Named.DISPLAY_P3), ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB)); } @Test public void loadOpaquePngResource_withNoOtherLoaders_decodesResource() { Glide.get(context) .getRegistry() .prepend( Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.canonical_png)); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadTransparentPngResource_withNoOtherLoaders_decodesResource() { Glide.get(context) .getRegistry() .prepend( Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.canonical_transparent_png)); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadTransparentGifResource_withNoOtherLoaders_decodesResource() { Glide.get(context) .getRegistry() .prepend( Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.transparent_gif)); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadTransparentGifResource_asHardware_withNoOtherLoaders_decodesResource() throws InterruptedException { assumeTrue( "Hardware Bitmaps are only supported on P+", Build.VERSION.SDK_INT >= Build.VERSION_CODES.P); // enableHardwareBitmaps must be called on the main thread. final CountDownLatch latch = new CountDownLatch(1); Util.postOnUiThread( new Runnable() { @Override public void run() { Glide.enableHardwareBitmaps(); latch.countDown(); } }); latch.await(5, TimeUnit.SECONDS); Glide.get(context) .getRegistry() .prepend( Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.transparent_gif)); Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .set(Downsampler.ALLOW_HARDWARE_CONFIG, true) .format(DecodeFormat.PREFER_ARGB_8888) .load(new Object()) .submit()); assertThat(bitmap).isNotNull(); assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.HARDWARE); } @Test public void loadTransparentGifResource_withNoOtherLoaders_fromBytes_decodesResource() { byte[] data = getBytes(ResourceIds.raw.transparent_gif); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadOpaqueGifResource_withNoOtherLoaders_decodesResource() { Glide.get(context) .getRegistry() .prepend( Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.opaque_gif)); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadOpaqueGifResource_asBytes_decodesResource() { byte[] data = getBytes(ResourceIds.raw.opaque_gif); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(bitmap).isNotNull(); } @Test public void loadOpaqueGifResource_asHardware_withNoOtherLoaders_decodesResource() { assumeTrue( "Hardware Bitmaps are only supported on P+", Build.VERSION.SDK_INT >= Build.VERSION_CODES.P); Glide.get(context) .getRegistry() .prepend( Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.opaque_gif)); Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() // Allow HARDWARE Bitmaps. .format(DecodeFormat.PREFER_ARGB_8888) .load(new Object()) .submit()); assertThat(bitmap).isNotNull(); } private byte[] getBytes(int resourceId) { ByteArrayOutputStream os = new ByteArrayOutputStream(); InputStream is = null; try { is = context.getResources().openRawResource(resourceId); byte[] buffer = new byte[1024 * 1024]; int read; while ((read = is.read(buffer)) != -1) { os.write(buffer, 0, read); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored; } } } return os.toByteArray(); } private class FakeModelLoader implements ModelLoader, ModelLoaderFactory { private final int resourceId; FakeModelLoader(int resourceId) { this.resourceId = resourceId; } @androidx.annotation.Nullable @Override public LoadData buildLoadData( @NonNull Object o, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(o), new Fetcher()); } @Override public boolean handles(@NonNull Object o) { return true; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return this; } @Override public void teardown() {} private final class Fetcher implements DataFetcher { private InputStream inputStream; @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { inputStream = getInputStreamForResource(context, resourceId); callback.onDataReady(inputStream); } private InputStream getInputStreamForResource(Context context, @DrawableRes int resourceId) { Resources resources = context.getResources(); try { Uri parse = Uri.parse( String.format( Locale.US, "%s://%s/%s/%s", ContentResolver.SCHEME_ANDROID_RESOURCE, resources.getResourcePackageName(resourceId), resources.getResourceTypeName(resourceId), resources.getResourceEntryName(resourceId))); return context.getContentResolver().openInputStream(parse); } catch (Resources.NotFoundException | FileNotFoundException e) { throw new IllegalArgumentException("Resource ID " + resourceId + " not found", e); } } @Override public void cleanup() { InputStream local = inputStream; if (local != null) { try { local.close(); } catch (IOException e) { // Ignored. } } } @Override public void cancel() { // Do nothing. } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/LoadVideoResourceTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.IOException; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; /** * Tests that Glide is able to load videos stored in resources and loaded as {@link * android.content.res.AssetFileDescriptor}s. */ @RunWith(AndroidJUnit4.class) public class LoadVideoResourceTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); } @Test public void loadVideoResourceId_fromInt_decodesFrame() { Drawable frame = concurrency.get(Glide.with(context).load(ResourceIds.raw.video).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceId_fromInt_withFrameTime_decodesFrame() { Drawable frame = concurrency.get( GlideApp.with(context) .load(ResourceIds.raw.video) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } // Testing boxed integer. @SuppressWarnings("UnnecessaryBoxing") @Test public void loadVideoResourceId_fromInteger_decodesFrame() { Drawable frame = concurrency.get(Glide.with(context).load(new Integer(ResourceIds.raw.video)).submit()); assertThat(frame).isNotNull(); } // Testing boxed integer. @SuppressWarnings("UnnecessaryBoxing") @Test public void loadVideoResourceId_fromInteger_withFrameTime_decodesFrame() { Drawable frame = concurrency.get( GlideApp.with(context) .load(new Integer(ResourceIds.raw.video)) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceId_asBitmap_decodesFrame() { Bitmap frame = concurrency.get(Glide.with(context).asBitmap().load(ResourceIds.raw.video).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceId_asBitmap_withFrameTime_decodesFrame() { Bitmap frame = concurrency.get( GlideApp.with(context) .asBitmap() .load(ResourceIds.raw.video) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUri_fromId_decodesFrame() { Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path(String.valueOf(ResourceIds.raw.video)) .build(); Drawable frame = concurrency.get(GlideApp.with(context).load(uri).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUri_asBitmap_fromId_decodesFrame() { Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path(String.valueOf(ResourceIds.raw.video)) .build(); Bitmap frame = concurrency.get(GlideApp.with(context).asBitmap().load(uri).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUri_fromId_withFrame_decodesFrame() { Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path(String.valueOf(ResourceIds.raw.video)) .build(); Bitmap frame = concurrency.get( GlideApp.with(context) .asBitmap() .load(uri) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUriString_fromId_decodesFrame() { Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path(String.valueOf(ResourceIds.raw.video)) .build(); Bitmap frame = concurrency.get(GlideApp.with(context).asBitmap().load(uri.toString()).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUriString_fromId_withFrame_decodesFrame() { Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path(String.valueOf(ResourceIds.raw.video)) .build(); Bitmap frame = concurrency.get( GlideApp.with(context) .asBitmap() .load(uri.toString()) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUri_fromName_decodesFrame() { Resources resources = context.getResources(); int resourceId = ResourceIds.raw.video; Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(resources.getResourcePackageName(resourceId)) .appendPath(resources.getResourceTypeName(resourceId)) .appendPath(resources.getResourceEntryName(resourceId)) .build(); Drawable frame = concurrency.get(GlideApp.with(context).load(uri).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUri_asBitmap_fromName_decodesFrame() { Resources resources = context.getResources(); int resourceId = ResourceIds.raw.video; Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(resources.getResourcePackageName(resourceId)) .appendPath(resources.getResourceTypeName(resourceId)) .appendPath(resources.getResourceEntryName(resourceId)) .build(); Bitmap frame = concurrency.get(GlideApp.with(context).asBitmap().load(uri).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUri_fromName_withFrame_decodesFrame() { Resources resources = context.getResources(); int resourceId = ResourceIds.raw.video; Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(resources.getResourcePackageName(resourceId)) .appendPath(resources.getResourceTypeName(resourceId)) .appendPath(resources.getResourceEntryName(resourceId)) .build(); Bitmap frame = concurrency.get( GlideApp.with(context) .asBitmap() .load(uri) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUriString_fromName_decodesFrame() { Resources resources = context.getResources(); int resourceId = ResourceIds.raw.video; Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(resources.getResourcePackageName(resourceId)) .appendPath(resources.getResourceTypeName(resourceId)) .appendPath(resources.getResourceEntryName(resourceId)) .build(); Bitmap frame = concurrency.get(GlideApp.with(context).asBitmap().load(uri.toString()).submit()); assertThat(frame).isNotNull(); } @Test public void loadVideoResourceUriString_fromName_withFrame_decodesFrame() { Resources resources = context.getResources(); int resourceId = ResourceIds.raw.video; Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(resources.getResourcePackageName(resourceId)) .appendPath(resources.getResourceTypeName(resourceId)) .appendPath(resources.getResourceEntryName(resourceId)) .build(); Bitmap frame = concurrency.get( GlideApp.with(context) .asBitmap() .load(uri.toString()) .frame(TimeUnit.SECONDS.toMicros(1)) .submit()); assertThat(frame).isNotNull(); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/MultiRequestTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.test.ModelGeneratorRule; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MultiRequestTest { private final Context context = ApplicationProvider.getApplicationContext(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); @Rule public final ModelGeneratorRule modelGeneratorRule = new ModelGeneratorRule(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); @Test public void thumbnail_onResourceReady_forPrimary_isComplete_whenRequestListenerIsCalled() throws IOException, InterruptedException { // Make sure the requests complete in the same order Glide.init( context, new GlideBuilder() .setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build())); AtomicBoolean isPrimaryRequestComplete = new AtomicBoolean(false); CountDownLatch countDownLatch = new CountDownLatch(1); RequestBuilder request = Glide.with(context) .load(newImageFile()) .thumbnail(Glide.with(context).load(newImageFile())) .listener( new RequestListener<>() { @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( @NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { isPrimaryRequestComplete.set(target.getRequest().isComplete()); countDownLatch.countDown(); return false; } }); concurrency.runOnMainThread(() -> request.into(new DoNothingTarget())); assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue(); assertThat(isPrimaryRequestComplete.get()).isTrue(); } @Test public void thumbnail_onLoadFailed_forPrimary_isNotRunningOrComplete_whenRequestListenerIsCalled() throws IOException, InterruptedException { // Make sure the requests complete in the same order Glide.init( context, new GlideBuilder() .setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build())); AtomicBoolean isNeitherRunningNorComplete = new AtomicBoolean(false); CountDownLatch countDownLatch = new CountDownLatch(1); int missingResourceId = 123; RequestBuilder requestBuilder = Glide.with(context) .load(missingResourceId) .thumbnail(Glide.with(context).load(newImageFile())) .listener( new RequestListener<>() { @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { Request request = target.getRequest(); isNeitherRunningNorComplete.set(!request.isComplete() && !request.isRunning()); countDownLatch.countDown(); return false; } @Override public boolean onResourceReady( @NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { return false; } }); concurrency.runOnMainThread(() -> requestBuilder.into(new DoNothingTarget())); assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue(); assertThat(isNeitherRunningNorComplete.get()).isTrue(); } private File newImageFile() throws IOException { Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawColor(Color.RED); File result = temporaryFolder.newFile(); try (OutputStream os = new BufferedOutputStream(new FileOutputStream(result))) { bitmap.compress(CompressFormat.JPEG, 75, os); } return result; } // We don't store or do anything with the resource, so we don't need to do anything to release it // in onLoadCleared. private static final class DoNothingTarget extends CustomTarget { @Override public void onResourceReady( @NonNull Drawable resource, @Nullable Transition transition) {} @Override public void onLoadCleared(@Nullable Drawable placeholder) {} } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/NonBitmapDrawableResourcesTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.request.RequestOptions.bitmapTransform; import static com.bumptech.glide.request.RequestOptions.centerCropTransform; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.TearDownGlide; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.rules.TestName; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class NonBitmapDrawableResourcesTest { @Rule public final TestName testName = new TestName(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final Context context = ApplicationProvider.getApplicationContext(); @Test public void load_withBitmapResourceId_asDrawable_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context).load(android.R.drawable.star_big_off).submit().get(); assertThat(drawable).isNotNull(); } @Test public void load_withBitmapResourceId_asDrawable_withTransformation_producesNonNullBitmap() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context) .load(android.R.drawable.star_big_off) .apply(centerCropTransform()) .submit() .get(); assertThat(drawable).isNotNull(); } @Test public void load_withBitmapResourceId_asBitmap_producesNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context).asBitmap().load(android.R.drawable.star_big_off).submit().get(); assertThat(bitmap).isNotNull(); } @Test public void load_withBitmapAliasResourceId_asDrawable_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context).load(ResourceIds.drawable.bitmap_alias).submit().get(); assertThat(drawable).isNotNull(); } @Test public void load_withBitmapAliasResourceId_asDrawable_withTransformation_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context) .load(ResourceIds.drawable.bitmap_alias) .apply(centerCropTransform()) .submit() .get(); assertThat(drawable).isNotNull(); } @Test public void load_withBitmapAliasResourceId_asBitmap_producesNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context).asBitmap().load(ResourceIds.drawable.bitmap_alias).submit().get(); assertThat(bitmap).isNotNull(); } @Test public void load_withShapeDrawableResourceId_asDrawable_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context).load(ResourceIds.drawable.shape_drawable).submit().get(); assertThat(drawable).isNotNull(); } @Test public void load_withShapeDrawableResourceId_asDrawable_withTransformation_sizeOriginal_fails() throws ExecutionException, InterruptedException { assertThrows( ExecutionException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { Glide.with(context) .load(ResourceIds.drawable.shape_drawable) .apply(centerCropTransform()) .submit() .get(); } }); } @Test public void load_withShapeDrawableResourceId_asDrawable_withTransformation_validSize_succeeds() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context) .load(ResourceIds.drawable.shape_drawable) .apply(bitmapTransform(new RoundedCorners(10))) .submit(100, 200) .get(); assertThat(drawable).isNotNull(); assertThat(drawable.getIntrinsicWidth()).isEqualTo(100); assertThat(drawable.getIntrinsicHeight()).isEqualTo(200); } @Test public void load_withShapeDrawableResourceId_asBitmap_withSizeOriginal_fails() throws ExecutionException, InterruptedException { assertThrows( ExecutionException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { Glide.with(context).asBitmap().load(ResourceIds.drawable.shape_drawable).submit().get(); } }); } @Test public void load_withShapeDrawableResourceId_asBitmap_withValidSize_returnsNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context) .asBitmap() .load(ResourceIds.drawable.shape_drawable) .submit(100, 200) .get(); assertThat(bitmap).isNotNull(); assertThat(bitmap.getWidth()).isEqualTo(100); assertThat(bitmap.getHeight()).isEqualTo(200); } @Test public void load_withShapeDrawableResourceId_asBitmap_withValidSizeAndTransform_nonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context) .asBitmap() .load(ResourceIds.drawable.shape_drawable) .apply(centerCropTransform()) .submit(100, 200) .get(); assertThat(bitmap).isNotNull(); assertThat(bitmap.getWidth()).isEqualTo(100); assertThat(bitmap.getHeight()).isEqualTo(200); } @Test public void load_withStateListDrawableResourceId_asDrawable_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context).load(ResourceIds.drawable.state_list_drawable).submit().get(); assertThat(drawable).isNotNull(); } @Test public void load_withStateListDrawableResourceId_asDrawable_withTransformation_nonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context) .load(ResourceIds.drawable.state_list_drawable) .apply(centerCropTransform()) .submit() .get(); assertThat(drawable).isNotNull(); } @Test public void load_withStateListDrawableResourceId_asBitmap_producesNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context) .asBitmap() .load(ResourceIds.drawable.state_list_drawable) .submit() .get(); assertThat(bitmap).isNotNull(); } @Test public void load_withStateListDrawableResourceId_asBitmap_withTransformation_nonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context) .asBitmap() .load(ResourceIds.drawable.state_list_drawable) .apply(centerCropTransform()) .submit() .get(); assertThat(bitmap).isNotNull(); } @Test public void load_withVectorDrawableResourceId_asDrawable_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context).load(ResourceIds.drawable.vector_drawable).submit().get(); assertThat(drawable).isNotNull(); } @Test public void load_withVectorDrawableResourceId_asDrawable_withTransformation_nonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context) .load(ResourceIds.drawable.vector_drawable) .apply(centerCropTransform()) .submit() .get(); assertThat(drawable).isNotNull(); } @Test public void load_withVectorDrawableResourceId_asBitmap_producesNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context).asBitmap().load(ResourceIds.drawable.vector_drawable).submit().get(); assertThat(bitmap).isNotNull(); } @Test public void load_withVectorDrawableResourceId_asBitmap_withTransformation_producesNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context) .asBitmap() .load(ResourceIds.drawable.vector_drawable) .apply(centerCropTransform()) .submit() .get(); assertThat(bitmap).isNotNull(); } @Test public void load_withNinePatchResourceId_asDrawable_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context).load(ResourceIds.drawable.googlelogo_color_120x44dp).submit().get(); assertThat(drawable).isNotNull(); } @Test public void load_withNinePatchResourceId_asDrawable_withTransformation_producesNonNullDrawable() throws ExecutionException, InterruptedException { Drawable drawable = Glide.with(context) .load(ResourceIds.drawable.googlelogo_color_120x44dp) .apply(centerCropTransform()) .submit() .get(); assertThat(drawable).isNotNull(); } @Test public void load_withNinePatchResourceId_asBitmap_producesNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context) .asBitmap() .load(ResourceIds.drawable.googlelogo_color_120x44dp) .submit() .get(); assertThat(bitmap).isNotNull(); } @Test public void load_withNinePatchResourceId_asBitmap_withTransformation_producesNonNullBitmap() throws ExecutionException, InterruptedException { Bitmap bitmap = Glide.with(context) .asBitmap() .load(ResourceIds.drawable.googlelogo_color_120x44dp) .apply(centerCropTransform()) .submit() .get(); assertThat(bitmap).isNotNull(); } @Test public void load_withApplicationIconResourceIdUri_asDrawable_producesNonNullDrawable() throws NameNotFoundException, ExecutionException, InterruptedException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .path(String.valueOf(iconResourceId)) .build(); Drawable drawable = Glide.with(context).load(uri).submit().get(); assertThat(drawable).isNotNull(); } } @Test public void load_withApplicationIconResourceIdUri_asDrawable_withTransformation_nonNullDrawable() throws NameNotFoundException, ExecutionException, InterruptedException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .path(String.valueOf(iconResourceId)) .build(); Drawable drawable = Glide.with(context).load(uri).apply(centerCropTransform()).submit().get(); assertThat(drawable).isNotNull(); } } @Test public void load_withApplicationIconResourceIdUri_asBitmap_producesNonNullBitmap() throws NameNotFoundException, ExecutionException, InterruptedException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .path(String.valueOf(iconResourceId)) .build(); Bitmap bitmap = Glide.with(context).asBitmap().load(uri).submit().get(); assertThat(bitmap).isNotNull(); } } @Test public void load_withApplicationIconResourceIdUri_asBitmap_withTransformation_nonNullBitmap() throws ExecutionException, InterruptedException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .path(String.valueOf(iconResourceId)) .build(); Bitmap bitmap = Glide.with(context).asBitmap().apply(centerCropTransform()).load(uri).submit().get(); assertThat(bitmap).isNotNull(); } } @Test public void load_withApplicationIconResourceNameUri_asDrawable_producesNonNullDrawable() throws ExecutionException, InterruptedException, NameNotFoundException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Context toUse = context.createPackageContext(packageName, /* flags= */ 0); Resources resources = toUse.getResources(); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .appendPath(resources.getResourceTypeName(iconResourceId)) .appendPath(resources.getResourceEntryName(iconResourceId)) .build(); Drawable drawable = Glide.with(context).load(uri).submit().get(); assertThat(drawable).isNotNull(); } } @Test public void load_withApplicationIconResourceNameUri_asDrawable_withTransform_nonNullDrawable() throws ExecutionException, InterruptedException, NameNotFoundException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Context toUse = context.createPackageContext(packageName, /* flags= */ 0); Resources resources = toUse.getResources(); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .appendPath(resources.getResourceTypeName(iconResourceId)) .appendPath(resources.getResourceEntryName(iconResourceId)) .build(); Drawable drawable = Glide.with(context).load(uri).apply(centerCropTransform()).submit().get(); assertThat(drawable).isNotNull(); } } @Test public void load_withApplicationIconResourceNameUri_asBitmap_producesNonNullBitmap() throws ExecutionException, InterruptedException, NameNotFoundException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Context toUse = context.createPackageContext(packageName, /* flags= */ 0); Resources resources = toUse.getResources(); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .appendPath(resources.getResourceTypeName(iconResourceId)) .appendPath(resources.getResourceEntryName(iconResourceId)) .build(); Bitmap bitmap = Glide.with(context).asBitmap().load(uri).submit().get(); assertThat(bitmap).isNotNull(); } } @Test public void load_withApplicationIconResourceNameUri_asBitmap_withTransform_nonNullBitmap() throws ExecutionException, InterruptedException, NameNotFoundException { for (String packageName : getInstalledPackages()) { int iconResourceId = getResourceId(packageName); Context toUse = context.createPackageContext(packageName, /* flags= */ 0); Resources resources = toUse.getResources(); Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) .appendPath(resources.getResourceTypeName(iconResourceId)) .appendPath(resources.getResourceEntryName(iconResourceId)) .build(); Bitmap bitmap = Glide.with(context).asBitmap().apply(centerCropTransform()).load(uri).submit().get(); assertThat(bitmap).isNotNull(); } } private Set getInstalledPackages() { Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); PackageManager packageManager = context.getPackageManager(); List pkgAppsList = packageManager.queryIntentActivities(mainIntent, /* flags= */ 0); Set result = new HashSet<>(); for (ResolveInfo info : pkgAppsList) { String packageName = info.activityInfo.packageName; int iconResourceId = getResourceId(packageName); if (iconResourceId != 0 && doesApplicationPackageNameMatchResourcePackageName(packageName, iconResourceId)) { result.add(info.activityInfo.packageName); } } return result; } private int getResourceId(String packageName) { PackageInfo packageInfo; try { packageInfo = context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0); } catch (NameNotFoundException e) { return 0; } return packageInfo.applicationInfo.icon; } /** * Returns {@code true} iff the resource package name is exactly the same as the containing * application package name for a given resource id. * *

The resource package name is the value returned by {@link * Resources#getResourcePackageName(int)}. The application package name is package name of the * enclosing application. If these two things are equal, then we can both construct a Context for * that package and retrieve a resource id for that package from a "standard" resource Uri * containing a name instead of an id. If they aren't equal, then we can do only one of the two * required tasks, so our Uri load will always fail. To handle this properly, we'd need callers to * include both package names in the Uri. I'm not aware of any standardized Uri format for doing * so, so these requests will just be treated as unsupported for the time being. * *

Take Calendar (emulators API 24 and below) as an example: * *

    *
  • package name: com.google.android.calendar *
  • resource package name: com.android.calendar *
* * We can construct one of two possible Uris: * *
    *
  • android.resource://com.google.android.calendar/mipmap/ic_icon_calendar. *
  • android.resource://com.android.calendar/mipmap/ic_icon_calendar.< *
* * From the first Uri, we can obtain the correct Context/Resources for the calendar package, but * our attempts to resolve the correct resource id will fail because we do not have the resource * package name. From the second Uri we cannot obtain the Context/Resources for the calendar * package because the resource package name doesn't match the application package name. */ private boolean doesApplicationPackageNameMatchResourcePackageName( String applicationPackageName, int iconResourceId) { try { Context current = context.createPackageContext(applicationPackageName, /* flags= */ 0); String resourcePackageName = current.getResources().getResourcePackageName(iconResourceId); return applicationPackageName.equals(resourcePackageName); } catch (NameNotFoundException e) { // This should never happen throw new RuntimeException(e); } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/PausedRequestsTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.GlideRequests; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import org.junit.Rule; import org.junit.Test; /** * Tests how {@link com.bumptech.glide.request.Request}s behave when the corresponding {@link * RequestManager} is paused. */ public final class PausedRequestsTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private final Context context = ApplicationProvider.getApplicationContext(); @SuppressWarnings("unchecked") @Test public void load_withPlaceHolderSet_requestsPaused_displaysPlaceholder() { final ImageView imageView = new ImageView(context); final GlideRequests requests = GlideApp.with(context); concurrency.runOnMainThread( new Runnable() { @Override public void run() { requests.pauseAllRequests(); } }); final ColorDrawable expected = new ColorDrawable(Color.RED); concurrency.runOnMainThread( new Runnable() { @Override public void run() { requests.load(ResourceIds.drawable.bitmap_alias).placeholder(expected).into(imageView); } }); assertThat(imageView.getDrawable()).isEqualTo(expected); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/RequestManagerLifecycleTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.Lifecycle.Event; import androidx.lifecycle.Lifecycle.State; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.OnLifecycleEvent; import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ActivityScenario.ActivityAction; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.instrumentation.R; import com.bumptech.glide.test.DefaultFragmentActivity; import com.bumptech.glide.testutil.TearDownGlide; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; // This test avoids using FragmentScenario because it doesn't seem to let us to get into the common // created but not yet started state, only either before onCreateView or after onResume. @RunWith(AndroidJUnit4.class) public class RequestManagerLifecycleTest { private static final String FRAGMENT_TAG = "fragment"; private static final String FRAGMENT_SIBLING_TAG = "fragment_sibling"; private static final String CHILD_FRAGMENT_TAG = "child"; @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); @Rule public final ActivityScenarioRule scenarioRule = new ActivityScenarioRule<>(DefaultFragmentActivity.class); private ActivityScenario scenario; @Before public void setUp() { scenario = scenarioRule.getScenario(); } @Test public void get_twice_withSameActivity_returnsSameRequestManager() { scenario.moveToState(State.CREATED); scenario.onActivity( activity -> assertThat(Glide.with(activity)).isEqualTo(Glide.with(activity))); } @Test public void get_withActivityBeforeCreate_startsRequestManager() { scenario.moveToState(State.CREATED); scenario.onActivity(activity -> assertThat(Glide.with(activity).isPaused()).isFalse()); } // See b/262668610 @SuppressWarnings("OnLifecycleEvent") @Test public void get_withActivityOnDestroy_QPlus_doesNotCrash() { // Activity#isDestroyed's behavior seems to have changed in Q. On Q+, isDestroyed returns false // during onDestroy, so we have to handle that case explicitly. assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); scenario.moveToState(State.CREATED); class GetOnDestroy implements LifecycleObserver { private final FragmentActivity activity; GetOnDestroy(FragmentActivity activity) { this.activity = activity; } @OnLifecycleEvent(Event.ON_DESTROY) public void onDestroy(@NonNull LifecycleOwner owner) { Glide.with(activity); } } scenario.onActivity( activity -> activity.getLifecycle().addObserver(new GetOnDestroy(activity))); scenario.moveToState(State.DESTROYED); } @SuppressWarnings("OnLifecycleEvent") @Test public void get_withActivityOnDestroy_afterJellyBeanAndbeforeQ_doesNotCrash() { // Activity#isDestroyed's behavior seems to have changed in Q. On Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q); AtomicReference thrownException = new AtomicReference<>(); scenario.moveToState(State.CREATED); class GetOnDestroy implements LifecycleObserver { private final FragmentActivity activity; GetOnDestroy(FragmentActivity activity) { this.activity = activity; } @OnLifecycleEvent(Event.ON_DESTROY) public void onDestroy(@NonNull LifecycleOwner owner) { try { Glide.with(activity); fail("Failed to throw expected exception"); } catch (Exception e) { thrownException.set(e); } } } scenario.onActivity( activity -> activity.getLifecycle().addObserver(new GetOnDestroy(activity))); scenario.moveToState(State.DESTROYED); assertThat(thrownException.get()) .hasMessageThat() .contains("You cannot start a load for a destroyed activity"); } @Test public void get_withFragment_beforeFragmentIsAdded_throws() { Fragment fragment = new Fragment(); assertThrows(NullPointerException.class, () -> Glide.with(fragment)); } @Test public void get_withFragment_whenFragmentIsAddedAndVisible_beforeStart_startsRequestManager() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); assertThat(fragment.isVisible()).isTrue(); assertThat(Glide.with(fragment).isPaused()).isFalse(); }); } @Test public void requestManager_afterFragmentIsStopped_isPaused() { // Avoid using FragmentScenario because it doesn't seem to let us to get into the common created // but not yet started state, only either before onCreateView or after onResume. final Fragment fragment = new EmptyContainerFragment(); scenario.moveToState(State.RESUMED); scenario.onActivity( activity -> { activity .getSupportFragmentManager() .beginTransaction() .add(R.id.container, fragment) .commitNowAllowingStateLoss(); // If we call with() for the first time after the fragment is paused but while it's still // visible, then we'll default the request manager to started. So we call with() once here // to make sure the request manager is created before the stop event below. Glide.with(fragment); }); scenario.moveToState(State.CREATED); scenario.onActivity( activity -> { assertThat(fragment.isVisible()).isTrue(); assertThat(Glide.with(fragment).isPaused()).isTrue(); }); } @Test public void get_twice_withSameFragment_returnsSameRequestManager() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); assertThat(Glide.with(fragment)).isEqualTo(Glide.with(fragment)); }); } @Test public void pauseRequestsRecursive_onActivity_pausesFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); Glide.with(activity).pauseAllRequestsRecursive(); assertThat(Glide.with(fragment).isPaused()).isTrue(); }); } @Test public void resumeRequestsRecursive_onActivity_resumesFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); Glide.with(activity).pauseAllRequestsRecursive(); Glide.with(activity).resumeRequestsRecursive(); assertThat(Glide.with(fragment).isPaused()).isFalse(); }); } @Test public void pauseRequestsRecursive_onActivity_pausesChildOfChildFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment childFragment = getChildFragment(activity); Glide.with(activity).pauseAllRequestsRecursive(); assertThat(Glide.with(childFragment).isPaused()).isTrue(); }); } @Test public void resumeRequestsRecursive_onActivity_resumesChildOfChildFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment childFragment = getChildFragment(activity); Glide.with(activity).pauseAllRequestsRecursive(); Glide.with(activity).resumeRequestsRecursive(); assertThat(Glide.with(childFragment).isPaused()).isFalse(); }); } @Test public void pauseRequestsRecursive_onChildFragmentOfActivity_doesNotPauseActivity() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); Glide.with(fragment).pauseAllRequestsRecursive(); assertThat(Glide.with(fragment).isPaused()).isTrue(); assertThat(Glide.with(activity).isPaused()).isFalse(); }); } @Test public void pauseRequestsRecursive_onChildFragmentOfActivity_pausesChildOfChildFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment parentFragment = getFragment(activity); Fragment childFragment = getChildFragment(activity); Glide.with(parentFragment).pauseAllRequestsRecursive(); assertThat(Glide.with(childFragment).isPaused()).isTrue(); }); } @Test public void resumeRequestsRecursive_onChildFragmentOfActivity_resumesChildOfChildFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment parentFragment = getFragment(activity); Fragment childFragment = getChildFragment(activity); Glide.with(parentFragment).pauseAllRequestsRecursive(); Glide.with(parentFragment).resumeRequestsRecursive(); assertThat(Glide.with(childFragment).isPaused()).isFalse(); }); } @Test public void pauseRequests_onActivity_pausesRequestManager() { scenario.moveToState(State.RESUMED); scenario.onActivity( activity -> { Glide.with(activity).pauseAllRequests(); assertThat(Glide.with(activity).isPaused()).isTrue(); }); } @Test public void resumeRequests_onActivity_pausesRequestManager() { scenario.moveToState(State.RESUMED); scenario.onActivity( activity -> { Glide.with(activity).pauseAllRequests(); Glide.with(activity).resumeRequests(); assertThat(Glide.with(activity).isPaused()).isFalse(); }); } @Test public void pauseRequests_onActivity_doesNotPauseChildren() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); initRequestManagers(activity, fragment); Glide.with(activity).pauseAllRequests(); assertThat(Glide.with(fragment).isPaused()).isFalse(); }); } @Test public void resumeRequests_onActivity_doesNotResumeChildren() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); initRequestManagers(activity, fragment); Glide.with(activity).pauseAllRequests(); Glide.with(fragment).pauseAllRequests(); Glide.with(activity).resumeRequests(); assertThat(Glide.with(fragment).isPaused()).isTrue(); }); } @Test public void pauseRequests_onFragment_pausesRequestManager() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); Glide.with(fragment).pauseAllRequests(); assertThat(Glide.with(fragment).isPaused()).isTrue(); }); } @Test public void resumeRequests_onFragment_resumesRequestManager() { withActivityFragmentAndChildFragment( activity -> { Fragment fragment = getFragment(activity); Glide.with(fragment).pauseAllRequests(); Glide.with(fragment).resumeRequests(); assertThat(Glide.with(fragment).isPaused()).isFalse(); }); } @Test public void pauseRequests_onChildFragment_doesNotPauseParentFragment() { withActivityFragmentAndChildFragment( activity -> { Glide.with(getChildFragment(activity)).pauseAllRequests(); assertThat(Glide.with(getFragment(activity)).isPaused()).isFalse(); }); } @Test public void resumeRequests_onChildFragment_doesNotResumeParentFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment parentFragment = getFragment(activity); Fragment childFragment = getChildFragment(activity); Glide.with(childFragment).pauseAllRequests(); Glide.with(parentFragment).pauseAllRequests(); Glide.with(childFragment).resumeRequests(); assertThat(Glide.with(parentFragment).isPaused()).isTrue(); }); } @Test public void pauseRequests_onChildFragment_pausesChildFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment childFragment = getChildFragment(activity); Glide.with(childFragment).pauseAllRequests(); assertThat(Glide.with(childFragment).isPaused()).isTrue(); }); } @Test public void resumeRequests_onChildFragment_resumesChildFragment() { withActivityFragmentAndChildFragment( activity -> { Fragment childFragment = getChildFragment(activity); Glide.with(childFragment).pauseAllRequests(); Glide.with(childFragment).resumeRequests(); assertThat(Glide.with(childFragment).isPaused()).isFalse(); }); } @Test public void pauseRequestsRecursive_onActivity_withTwoSiblingFragments_pausesBothSiblings() { withActivityAndTwoFragmentSiblings( activity -> { Fragment fragment = getFragment(activity); Fragment sibling = getSiblingFragment(activity); Glide.with(activity).pauseAllRequestsRecursive(); assertThat(Glide.with(fragment).isPaused()).isTrue(); assertThat(Glide.with(sibling).isPaused()).isTrue(); }); } @Test public void resumeRequestsRecursive_onActivity_withTwoSiblingFragments_resumesBothSiblings() { withActivityAndTwoFragmentSiblings( activity -> { Fragment fragment = getFragment(activity); Fragment sibling = getSiblingFragment(activity); Glide.with(activity).pauseAllRequestsRecursive(); Glide.with(activity).resumeRequestsRecursive(); assertThat(Glide.with(fragment).isPaused()).isFalse(); assertThat(Glide.with(sibling).isPaused()).isFalse(); }); } @Test public void pauseRequestsRecursive_onFragment_withSibling_doesNotPauseSibling() { withActivityAndTwoFragmentSiblings( activity -> { Fragment fragment = getFragment(activity); Fragment sibling = getSiblingFragment(activity); Glide.with(fragment).pauseAllRequestsRecursive(); assertThat(Glide.with(sibling).isPaused()).isFalse(); }); } @Test public void resumeRequestsRecursive_onFragment_withSibling_doesNotResumeSibling() { withActivityAndTwoFragmentSiblings( activity -> { Fragment fragment = getFragment(activity); Fragment sibling = getSiblingFragment(activity); Glide.with(fragment).pauseAllRequestsRecursive(); Glide.with(sibling).pauseAllRequests(); Glide.with(fragment).resumeRequestsRecursive(); assertThat(Glide.with(sibling).isPaused()).isTrue(); }); } // We need to create the RequestManager first, or else it will start in the paused state. // TODO(judds): If the parent is explicitly paused, any children added after it's paused should // probably default to paused when it's created? private void initRequestManagers(FragmentActivity activity, Fragment... fragments) { Glide.with(activity); for (Fragment fragment : fragments) { Glide.with(fragment); } } /** Creates the tree: Activity - Fragment - Fragment */ private void withActivityAndTwoFragmentSiblings( ActivityAction assertion) { setupAndRunActivityAction( activity -> { Fragment parentFragment = createAndAddFragment(activity, FRAGMENT_TAG); Fragment siblingFragment = createAndAddFragment(activity, FRAGMENT_SIBLING_TAG); initRequestManagers(activity, parentFragment, siblingFragment); }, assertion); } /** Creates the tree: Activity - Fragment - Child Fragment */ private void withActivityFragmentAndChildFragment( ActivityAction assertion) { setupAndRunActivityAction( activity -> { Fragment parentFragment = createAndAddFragment(activity, FRAGMENT_TAG); Fragment childFragment = createAndAddFragment(parentFragment, CHILD_FRAGMENT_TAG); initRequestManagers(activity, parentFragment, childFragment); }, assertion); } private void setupAndRunActivityAction( ActivityAction setup, ActivityAction assertion) { scenario.moveToState(State.RESUMED); // Using one onActivity call to do the test setup and another to assert gives the framework // and Glide's fragment management code (onAttach in particular) the opportunity to run before // our // assertions take place. scenario.onActivity(setup); scenario.onActivity(assertion); } private Fragment getFragment(FragmentActivity activity) { return getFragment(activity, FRAGMENT_TAG); } private Fragment getSiblingFragment(FragmentActivity activity) { return getFragment(activity, FRAGMENT_SIBLING_TAG); } private Fragment getChildFragment(FragmentActivity activity) { return getFragment(getFragment(activity).getChildFragmentManager(), CHILD_FRAGMENT_TAG); } private Fragment getFragment(FragmentActivity activity, String tag) { return getFragment(activity.getSupportFragmentManager(), tag); } private Fragment getFragment(FragmentManager manager, String tag) { return manager.findFragmentByTag(tag); } private Fragment createAndAddFragment(FragmentActivity parent, String tag) { return createAndAddFragment(parent.getSupportFragmentManager(), tag); } private Fragment createAndAddFragment(Fragment fragment, String tag) { return createAndAddFragment(fragment.getChildFragmentManager(), tag); } private Fragment createAndAddFragment(FragmentManager manager, String tag) { Fragment result = new EmptyContainerFragment(); manager.beginTransaction().add(R.id.container, result, tag).commitNowAllowingStateLoss(); return result; } public static final class EmptyContainerFragment extends Fragment { @Override public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate( R.layout.default_fragment_activity, container, /* attachToRoot= */ false); } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/RequestManagerTest.java ================================================ package com.bumptech.glide; import android.content.Context; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.manager.Lifecycle; import com.bumptech.glide.manager.LifecycleListener; import com.bumptech.glide.manager.RequestManagerTreeNode; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.test.ResourceIds.raw; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class RequestManagerTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private RequestManagerTreeNode treeNode; private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private RequestManager requestManager; private Context context; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); Glide glide = Glide.get(context); requestManager = new RequestManager( glide, new Lifecycle() { @Override public void addListener(@NonNull LifecycleListener listener) { listener.onStart(); } @Override public void removeListener(@NonNull LifecycleListener listener) { // Do nothing. } }, treeNode, context); } /** Tests #2262. */ @Test public void clear_withNonOwningRequestManager_afterOwningManagerIsDestroyed_doesNotThrow() { // First destroy our Fragment/Activity RequestManager. requestManager.onDestroy(); final ImageView imageView = new ImageView(context); imageView.measure(100, 100); imageView.layout(0, 0, 100, 100); // Then start a new load with our now destroyed RequestManager. concurrency.loadOnMainThread(requestManager.load(ResourceIds.raw.canonical), imageView); // Finally clear our new load with any RequestManager other than the one we used to start it. concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.with(context).clear(imageView); } }); } /** Tests b/69361054. */ @Test public void clear_withNonOwningRequestManager_onBackgroundThread_doesNotThrow() { concurrency.runOnMainThread( new Runnable() { @Override public void run() { requestManager.onDestroy(); } }); final Target target = concurrency.wait(requestManager.load(raw.canonical).submit()); concurrency.runOnMainThread( new Runnable() { @Override public void run() { Glide.with(context).clear(target); } }); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/RequestTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.testutil.WaitModelLoader; import com.bumptech.glide.testutil.WaitModelLoader.WaitModel; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** Tests the behaviors of Requests of all types. */ @RunWith(AndroidJUnit4.class) public class RequestTest { @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private RequestListener requestListener; private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private Context context; private ImageView imageView; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); imageView = new ImageView(context); imageView.measure(100, 100); imageView.layout(0, 0, 100, 100); // Some emulators only have a single resize thread, so waiting on a latch will block them // forever. Glide.init( context, new GlideBuilder().setSourceExecutor(GlideExecutor.newUnlimitedSourceExecutor())); } @Test public void clear_withSingleRequest_nullsOutDrawableInView() { concurrency.loadOnMainThread(GlideApp.with(context).load(ResourceIds.raw.canonical), imageView); assertThat(imageView.getDrawable()).isNotNull(); concurrency.clearOnMainThread(imageView); assertThat(imageView.getDrawable()).isNull(); } @Test public void clear_withRequestWithThumbnail_nullsOutDrawableInView() { concurrency.loadOnMainThread( GlideApp.with(context) .load(ResourceIds.raw.canonical) .thumbnail(GlideApp.with(context).load(ResourceIds.raw.canonical).override(100, 100)), imageView); assertThat(imageView.getDrawable()).isNotNull(); concurrency.clearOnMainThread(imageView); assertThat(imageView.getDrawable()).isNull(); } @Test public void onStop_withSingleRequest_doesNotNullOutDrawableInView() { concurrency.loadOnMainThread(GlideApp.with(context).load(ResourceIds.raw.canonical), imageView); assertThat(imageView.getDrawable()).isNotNull(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).onStop(); } }); assertThat(imageView.getDrawable()).isNotNull(); } @Test public void onStop_withRequestWithThumbnail_doesNotNullOutDrawableInView() { concurrency.loadOnMainThread( GlideApp.with(context) .load(ResourceIds.raw.canonical) .thumbnail(GlideApp.with(context).load(ResourceIds.raw.canonical).override(100, 100)), imageView); assertThat(imageView.getDrawable()).isNotNull(); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).onStop(); } }); assertThat(imageView.getDrawable()).isNotNull(); } @Test public void onStop_withSingleRequestInProgress_nullsOutDrawableInView() { final WaitModel model = WaitModelLoader.waitOn(ResourceIds.raw.canonical); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).load(ResourceIds.raw.canonical).into(imageView); } }); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).onStop(); } }); assertThat(imageView.getDrawable()).isNull(); model.countDown(); } @Test public void onStop_withRequestWithThumbnailBothInProgress_nullsOutDrawableInView() { final WaitModel model = WaitModelLoader.waitOn(ResourceIds.raw.canonical); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context) .load(model) .thumbnail(GlideApp.with(context).load(model).override(100, 100)) .into(imageView); } }); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).onStop(); } }); assertThat(imageView.getDrawable()).isNull(); model.countDown(); } /** Tests #2555. */ @Test public void clear_withRequestWithOnlyFullInProgress_nullsOutDrawableInView() { final WaitModel mainModel = WaitModelLoader.waitOn(ResourceIds.raw.canonical); concurrency.loadUntilFirstFinish( GlideApp.with(context) .load(mainModel) .listener(requestListener) .thumbnail( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .override(100, 100)), imageView); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).clear(imageView); } }); verify(requestListener, never()) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.DATA_DISK_CACHE), anyBoolean()); verify(requestListener, never()) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.RESOURCE_DISK_CACHE), anyBoolean()); assertThat(imageView.getDrawable()).isNull(); mainModel.countDown(); } @Test public void clear_withRequestWithOnlyFullInProgress_doesNotNullOutDrawableInView() { final WaitModel mainModel = WaitModelLoader.waitOn(ResourceIds.raw.canonical); concurrency.loadUntilFirstFinish( GlideApp.with(context) .load(mainModel) .listener(requestListener) .thumbnail( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .override(100, 100)), imageView); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).onStop(); } }); verify(requestListener, never()) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.DATA_DISK_CACHE), anyBoolean()); verify(requestListener, never()) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.RESOURCE_DISK_CACHE), anyBoolean()); assertThat(imageView.getDrawable()).isNotNull(); mainModel.countDown(); } @Test public void onStop_withRequestWithOnlyThumbnailInProgress_doesNotNullOutDrawableInView() { final WaitModel thumbModel = WaitModelLoader.waitOn(ResourceIds.raw.canonical); concurrency.loadUntilFirstFinish( GlideApp.with(context) .load(ResourceIds.raw.canonical) .listener(requestListener) .thumbnail( GlideApp.with(context) .load(thumbModel) .listener(requestListener) .override(100, 100)), imageView); concurrency.runOnMainThread( new Runnable() { @Override public void run() { GlideApp.with(context).onStop(); } }); verify(requestListener, never()) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.DATA_DISK_CACHE), anyBoolean()); verify(requestListener, never()) .onResourceReady( ArgumentMatchers.any(), any(), ArgumentMatchers.>any(), eq(DataSource.RESOURCE_DISK_CACHE), anyBoolean()); // Only requests that are running are paused in onStop. The full request should take priority // over the thumbnail request. Therefore, if the full request is finished in onStop, it should // not be cleared, even if the thumbnail request is still running. assertThat(imageView.getDrawable()).isNotNull(); thumbModel.countDown(); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/RoundedCornersRegressionTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.test.BitmapRegressionTester; import com.bumptech.glide.test.CanonicalBitmap; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.RegressionTest; import com.bumptech.glide.test.SplitByCpu; import com.bumptech.glide.test.SplitBySdk; import com.bumptech.glide.testutil.TearDownGlide; import java.util.concurrent.ExecutionException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.rules.TestRule; import org.junit.runner.RunWith; /** * Compares the output of RoundedCorners with canonical resource files for all SDKs Glide supports * and fails on deltas. */ @RunWith(AndroidJUnit4.class) @SplitByCpu @SplitBySdk({26, 24, 23, 21, 19, 18, 16}) @RegressionTest public class RoundedCornersRegressionTest { @Rule public final TestRule tearDownGlide = new TearDownGlide(); @Rule public final TestName testName = new TestName(); private Context context; private BitmapRegressionTester bitmapRegressionTester; private CanonicalBitmap canonicalBitmap; @Before public void setUp() throws Exception { context = ApplicationProvider.getApplicationContext(); bitmapRegressionTester = BitmapRegressionTester.newInstance(getClass(), testName).assumeShouldRun(); canonicalBitmap = new CanonicalBitmap(); } @Test public void testRoundedCorners() throws ExecutionException, InterruptedException { bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonicalBitmap.getBitmap()) .transform(new RoundedCorners(5))); } @Test public void testRoundedCorners_usePool() throws ExecutionException, InterruptedException { canonicalBitmap = canonicalBitmap.scale(0.1f); Bitmap redRect = createRect( Color.RED, canonicalBitmap.getWidth(), canonicalBitmap.getHeight(), Bitmap.Config.ARGB_8888); Glide.get(context).getBitmapPool().put(redRect); Bitmap roundedRect = bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonicalBitmap.getBitmap()) .override(canonicalBitmap.getWidth(), canonicalBitmap.getHeight()) .transform(new RoundedCorners(5))); assertThat(roundedRect).isEqualTo(redRect); } @Test public void testRoundedCorners_overRounded() throws ExecutionException, InterruptedException { bitmapRegressionTester.test( GlideApp.with(context) .asBitmap() .load(canonicalBitmap.getBitmap()) .transform(new RoundedCorners(20))); } private Bitmap createRect(int color, int width, int height, Bitmap.Config config) { final Bitmap result = Bitmap.createBitmap(width, height, config); Canvas canvas = new Canvas(result); canvas.drawColor(color); return result; } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/WideGamutTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.Config; import android.graphics.ColorSpace; import android.graphics.ColorSpace.Named; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import java.io.ByteArrayOutputStream; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class WideGamutTest { @Rule public final TestRule tearDownGlide = new TearDownGlide(); private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); private final Context context = ApplicationProvider.getApplicationContext(); @Before public void setUp() { assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); } @Test public void load_withWideGamutImage_returnsWideGamutBitmap() { Bitmap bitmap = concurrency.get( Glide.with(context).asBitmap().load(ResourceIds.raw.webkit_logo_p3).submit()); assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.RGBA_F16); } @Test public void load_withWideGamutImage_bitmapInPoolWithSizeAndConfig_usesBitmapFromPool() { int bitmapDimension = 1000; Glide.init( context, new GlideBuilder() .setBitmapPool(new LruBitmapPool(bitmapDimension * bitmapDimension * 8 * 4))); Bitmap expected = Bitmap.createBitmap(bitmapDimension, bitmapDimension, Bitmap.Config.RGBA_F16); Glide.get(context).getBitmapPool().put(expected); Bitmap bitmap = concurrency.get( Glide.with(context).asBitmap().load(ResourceIds.raw.webkit_logo_p3).submit()); assertThat(bitmap).isSameInstanceAs(expected); } // TODO: Even with hardware allowed, we get a wide F16. Attempting to decode the resource with // preferred config set to hardware fails with: // "D/skia (10312): --- Failed to allocate a hardware bitmap" @Test public void load_withWideGamutImage_hardwareAllowed_returnsDecodedBitmap() { Bitmap bitmap = concurrency.get( GlideApp.with(context) .asBitmap() .load(ResourceIds.raw.webkit_logo_p3) .set(Downsampler.ALLOW_HARDWARE_CONFIG, true) .submit()); assertThat(bitmap).isNotNull(); } @Test public void load_withEncodedPngWideGamutImage_decodesWideGamut() { Bitmap toCompress = Bitmap.createBitmap( 100, 100, Bitmap.Config.RGBA_F16, /* hasAlpha= */ true, ColorSpace.get(Named.DCI_P3)); byte[] data = asPng(toCompress); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.RGBA_F16); } @Test public void load_withEncodedJpegWideGamutImage_decodesArgb8888() { // TODO(b/71430152): Figure out whether or not this is supposed to pass in API 26 and fail in // API 27. assumeTrue(Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1); Bitmap toCompress = Bitmap.createBitmap( 100, 100, Bitmap.Config.RGBA_F16, /* hasAlpha= */ true, ColorSpace.get(Named.DCI_P3)); byte[] data = asJpeg(toCompress); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); } @Test public void load_withEncodedWebpWideGamutImage_decodesArgb8888() { Bitmap toCompress = Bitmap.createBitmap( 100, 100, Bitmap.Config.RGBA_F16, /* hasAlpha= */ true, ColorSpace.get(Named.DCI_P3)); byte[] data = asWebp(toCompress); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); } @Test public void load_withSmallerWideGamutInPool_decodesBitmap() { BitmapPool bitmapPool = Glide.get(context).getBitmapPool(); Bitmap toPut = Bitmap.createBitmap(300, 298, Config.RGBA_F16); bitmapPool.put(toPut); // Add a second Bitmap to account for the InputStream decode. bitmapPool.put(Bitmap.createBitmap(toPut)); Bitmap wideGamut = Bitmap.createBitmap(300, 300, Config.RGBA_F16); byte[] data = asPng(wideGamut); Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(bitmap).isNotNull(); } @Test public void circleCrop_withWideGamutBitmap_producesWideGamutBitmap() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.RGBA_F16); byte[] data = asPng(bitmap); Bitmap result = concurrency.get(GlideApp.with(context).asBitmap().load(data).circleCrop().submit()); assertThat(result).isNotNull(); assertThat(result.getConfig()).isEqualTo(Config.RGBA_F16); } @Test public void roundedCorners_withWideGamutBitmap_producesWideGamutBitmap() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.RGBA_F16); byte[] data = asPng(bitmap); Bitmap result = concurrency.get( GlideApp.with(context) .asBitmap() .load(data) .transform(new RoundedCorners(/* roundingRadius= */ 10)) .submit()); assertThat(result).isNotNull(); assertThat(result.getConfig()).isEqualTo(Config.RGBA_F16); } @Test public void loadWideGamutImage_withArgb888OfSufficientSizeInPool_usesArgb8888Bitmap() { Bitmap wideGamut = Bitmap.createBitmap(100, 50, Bitmap.Config.RGBA_F16); byte[] data = asPng(wideGamut); Bitmap argb8888 = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Glide.init( context, new GlideBuilder() .setBitmapPool(new LruBitmapPool(wideGamut.getAllocationByteCount() * 5))); Glide.get(context).getBitmapPool().put(argb8888); Bitmap result = concurrency.get(Glide.with(context).asBitmap().load(data).submit()); assertThat(result).isSameInstanceAs(argb8888); } private static byte[] asJpeg(Bitmap bitmap) { return toByteArray(bitmap, CompressFormat.JPEG); } private static byte[] asPng(Bitmap bitmap) { return toByteArray(bitmap, CompressFormat.PNG); } private static byte[] asWebp(Bitmap bitmap) { return toByteArray(bitmap, CompressFormat.WEBP); } private static byte[] toByteArray(Bitmap bitmap, CompressFormat format) { ByteArrayOutputStream os = new ByteArrayOutputStream(); assertThat(bitmap.compress(format, 100, os)).isTrue(); return os.toByteArray(); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/load/engine/executor/IdlingGlideRule.java ================================================ package com.bumptech.glide.load.engine.executor; import androidx.test.core.app.ApplicationProvider; import androidx.test.espresso.IdlingRegistry; import androidx.test.espresso.idling.concurrent.IdlingThreadPoolExecutor; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.function.UnaryOperator; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; /** Creates idling executors and registers them with espresso's {@link IdlingRegistry}. */ public final class IdlingGlideRule implements TestRule { private final UnaryOperator additionalOptions; public static IdlingGlideRule newGlideRule(UnaryOperator additionalOptions) { return new IdlingGlideRule(additionalOptions); } private IdlingGlideRule(UnaryOperator additionalOptions) { this.additionalOptions = additionalOptions; } @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { IdlingRegistry idlingRegistry = IdlingRegistry.getInstance(); IdlingThreadPoolExecutor sourceExecutor = newIdlingThreadPoolExecutor( GlideExecutor.DEFAULT_SOURCE_EXECUTOR_NAME, GlideExecutor.calculateBestThreadCount()); idlingRegistry.register(sourceExecutor); IdlingThreadPoolExecutor diskCacheExecutor = newIdlingThreadPoolExecutor( GlideExecutor.DEFAULT_DISK_CACHE_EXECUTOR_NAME, /* poolSize= */ GlideExecutor.DEFAULT_DISK_CACHE_EXECUTOR_THREADS); idlingRegistry.register(diskCacheExecutor); IdlingThreadPoolExecutor animationExecutor = newIdlingThreadPoolExecutor( GlideExecutor.DEFAULT_ANIMATION_EXECUTOR_NAME, GlideExecutor.calculateAnimationExecutorThreadCount()); idlingRegistry.register(animationExecutor); try { Glide.init( ApplicationProvider.getApplicationContext(), additionalOptions .apply(new GlideBuilder()) .setSourceExecutor(new GlideExecutor(sourceExecutor)) .setDiskCacheExecutor(new GlideExecutor(diskCacheExecutor)) .setAnimationExecutor(new GlideExecutor(animationExecutor))); base.evaluate(); } finally { idlingRegistry.unregister(sourceExecutor); idlingRegistry.unregister(diskCacheExecutor); idlingRegistry.unregister(animationExecutor); Glide.tearDown(); } } }; } private static IdlingThreadPoolExecutor newIdlingThreadPoolExecutor(String name, int poolSize) { return new IdlingThreadPoolExecutor( name, /* corePoolSize= */ poolSize, /* maximumPoolSize= */ poolSize, /* keepAliveTime= */ 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Thread::new); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/load/resource/bitmap/DownsamplerEmulatorTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static android.graphics.Bitmap.CompressFormat.JPEG; import static android.graphics.Bitmap.CompressFormat.PNG; import static android.graphics.Bitmap.CompressFormat.WEBP; import static android.os.Build.VERSION_CODES.KITKAT; import static com.bumptech.glide.load.resource.bitmap.DownsamplerEmulatorTest.Api.apis; import static com.bumptech.glide.load.resource.bitmap.DownsamplerEmulatorTest.Api.atAndAbove; import static com.bumptech.glide.load.resource.bitmap.DownsamplerEmulatorTest.Api.below; import static com.bumptech.glide.load.resource.bitmap.DownsamplerEmulatorTest.Api.onAllApisAndAllFormatsExpect; import static com.bumptech.glide.load.resource.bitmap.DownsamplerEmulatorTest.Formats.Builder.allFormats; import static com.bumptech.glide.load.resource.bitmap.DownsamplerEmulatorTest.Formats.Builder.formats; import static org.junit.Assert.fail; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.Config; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.util.DisplayMetrics; import androidx.annotation.Nullable; import androidx.exifinterface.media.ExifInterface; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.Preconditions; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; /** * Runs tests to make sure that DownsampleStrategy provides the output we expect. * *

WEBP at and above N rounds. Webp below N floors. PNG always floors. JPEG always rounds. */ @RunWith(AndroidJUnit4.class) @SuppressWarnings("VisibleForTests") public class DownsamplerEmulatorTest { @Test public void calculateScaling_withAtMost() throws IOException { new Tester(DownsampleStrategy.AT_MOST) // See #3673 .setTargetDimensions(1977, 2636) .givenImageWithDimensionsOf(3024, 4032, onAllApisAndAllFormatsExpect(1512, 2016)) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(400, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(300, onAllApisAndAllFormatsExpect(75, 75)) .givenImageWithDimensionsOf( 799, 100, atAndAbove(VERSION_CODES.N) .with(formats(JPEG, WEBP).expect(100, 13), formats(PNG).expect(99, 12)), below(VERSION_CODES.N) .with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(99, 12))) .givenImageWithDimensionsOf( 800, 100, atAndAbove(VERSION_CODES.N) .with(formats(JPEG, WEBP).expect(100, 13), formats(PNG).expect(100, 12)), below(VERSION_CODES.N) .with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12))) .givenImageWithDimensionsOf(801, 100, onAllApisAndAllFormatsExpect(50, 6)) .givenImageWithDimensionsOf( 100, 800, atAndAbove(VERSION_CODES.N) .with(formats(JPEG, WEBP).expect(13, 100), formats(PNG).expect(12, 100)), below(VERSION_CODES.N) .with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100))) .givenImageWithDimensionsOf( 801, 100, below(KITKAT) .with( // JPEG is correct because CENTER_INSIDE wants to give a subsequent // transformation an image that is greater in size than the requested size. On // Api > VERSION_CODES.KITKAT, CENTER_INSIDE can do the transformation itself. // On < VERSION_CODES.KITKAT, it has to assume a subsequent transformation will // be called. formats(JPEG).expect(50, 6), formats(PNG, WEBP).expect(50, 6))) .givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78)) // This set of examples demonstrate that webp uses round on N+ and floor < N. .setTargetDimensions(13, 13) .givenSquareImageWithDimensionOf( 99, atAndAbove(KITKAT) .with( // 99 / 8.0 = 12.375. ceil(12.375) = 13. round(12.375) = 12. floor(12.375) = 12. formats(JPEG).expect(13, 13), formats(PNG, WEBP).expect(12, 12)), below(KITKAT).with(formats(JPEG).expect(13, 13), formats(PNG, WEBP).expect(12, 12))) .givenSquareImageWithDimensionOf( 100, atAndAbove(VERSION_CODES.N) .with( // 100 / 8.0 = 12.5. ceil(12.5) = 13. round(12.5) = 13. floor(12.5) = 12. formats(JPEG, WEBP).expect(13, 13), formats(PNG).expect(12, 12)), below(VERSION_CODES.N) .with(formats(JPEG).expect(13, 13), formats(PNG, WEBP).expect(12, 12))) // Upscaling .setTargetDimensions(500, 500) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(200, 200)) .givenSquareImageWithDimensionOf(450, onAllApisAndAllFormatsExpect(450, 450)) .givenImageWithDimensionsOf(200, 450, onAllApisAndAllFormatsExpect(200, 450)) // Original scaling .setTargetDimensions(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .givenImageWithDimensionsOf(1821, 2634, onAllApisAndAllFormatsExpect(1821, 2634)) .run(); } @Test public void calculateScaling_withGainmap_androidU_withAtMost() throws IOException { new Tester(DownsampleStrategy.AT_MOST) // See #3673 .setTargetDimensions(1977, 2636) .givenGainmapImageWithDimensionsOf( 3024, 4032, /* allowHardwareConfig= */ false, atAndAbove(34) .with(new Formats(new CompressFormat[] {CompressFormat.JPEG}, 1512, 2016))) .givenGainmapImageWithDimensionsOf( 3024, 4032, /* allowHardwareConfig= */ true, atAndAbove(34) .with(new Formats(new CompressFormat[] {CompressFormat.JPEG}, 1512, 2016))) .run(); } @Test public void calculateScaling_withAtLeast() throws IOException { new Tester(DownsampleStrategy.AT_LEAST) // See #3673 .setTargetDimensions(1977, 2636) .givenImageWithDimensionsOf(3024, 4032, onAllApisAndAllFormatsExpect(3024, 4032)) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(300, onAllApisAndAllFormatsExpect(150, 150)) .givenImageWithDimensionsOf(799, 100, onAllApisAndAllFormatsExpect(799, 100)) .givenImageWithDimensionsOf(800, 100, onAllApisAndAllFormatsExpect(800, 100)) .givenImageWithDimensionsOf(801, 100, onAllApisAndAllFormatsExpect(801, 100)) .givenImageWithDimensionsOf(100, 800, onAllApisAndAllFormatsExpect(100, 800)) .givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78)) // Upscaling .setTargetDimensions(500, 500) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(200, 200)) .givenSquareImageWithDimensionOf(450, onAllApisAndAllFormatsExpect(450, 450)) .givenImageWithDimensionsOf(200, 450, onAllApisAndAllFormatsExpect(200, 450)) // Original scaling .setTargetDimensions(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .givenImageWithDimensionsOf(1821, 2634, onAllApisAndAllFormatsExpect(1821, 2634)) .run(); } @Test public void calculateScaling_withCenterInside() throws IOException { new Tester(DownsampleStrategy.CENTER_INSIDE) // See #3673 .setTargetDimensions(1977, 2636) .givenImageWithDimensionsOf( 3024, 4032, atAndAbove(KITKAT).with(allFormats().expect(1977, 2636)), below(KITKAT).with(allFormats().expect(3024, 4032))) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(400, onAllApisAndAllFormatsExpect(100, 100)) .givenImageWithDimensionsOf( 300, 300, atAndAbove(KITKAT).with(allFormats().expect(100, 100)), below(KITKAT).with(allFormats().expect(150, 150))) .givenImageWithDimensionsOf( 799, 100, atAndAbove(KITKAT).with(allFormats().expect(100, 13)), below(KITKAT).with(formats(JPEG).expect(200, 25), formats(PNG, WEBP).expect(199, 25))) .givenImageWithDimensionsOf( 800, 100, atAndAbove(KITKAT).with(allFormats().expect(100, 13)), below(KITKAT).with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12))) .givenImageWithDimensionsOf( 801, 100, atAndAbove(VERSION_CODES.N) .with(formats(JPEG, WEBP).expect(100, 13), formats(PNG).expect(100, 12)), apis(KITKAT, VERSION_CODES.M) .with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12)), below(KITKAT) .with( // JPEG is correct because CENTER_INSIDE wants to give a subsequent // transformation an image that is greater in size than the requested size. On // Api > VERSION_CODES.KITKAT, CENTER_INSIDE can do the transformation itself. // On < VERSION_CODES.KITKAT, it has to assume a subsequent transformation will // be called. formats(JPEG).expect(101, 13), formats(PNG, WEBP).expect(100, 12))) .givenImageWithDimensionsOf( 100, 800, atAndAbove(KITKAT).with(allFormats().expect(13, 100)), below(KITKAT).with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100))) .givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78)) .setTargetDimensions(897, 897) .givenImageWithDimensionsOf( 2208, 1520, atAndAbove(KITKAT).with(allFormats().expect(897, 618)), below(KITKAT).with(allFormats().expect(1104, 760))) // Upscaling .setTargetDimensions(500, 500) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(200, 200)) .givenSquareImageWithDimensionOf(450, onAllApisAndAllFormatsExpect(450, 450)) .givenImageWithDimensionsOf(200, 450, onAllApisAndAllFormatsExpect(200, 450)) // Original scaling .setTargetDimensions(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .givenImageWithDimensionsOf(1821, 2634, onAllApisAndAllFormatsExpect(1821, 2634)) .run(); } @Test public void calculateScaling_withCenterOutside() throws IOException { new Tester(DownsampleStrategy.CENTER_OUTSIDE) // See #3673 .setTargetDimensions(1977, 2636) .givenImageWithDimensionsOf( 3024, 4032, atAndAbove(KITKAT).with(allFormats().expect(1977, 2636)), below(KITKAT).with(allFormats().expect(3024, 4032))) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(400, onAllApisAndAllFormatsExpect(100, 100)) .givenImageWithDimensionsOf( 300, 300, atAndAbove(KITKAT).with(allFormats().expect(100, 100)), below(KITKAT).with(allFormats().expect(150, 150))) .givenImageWithDimensionsOf(799, 100, onAllApisAndAllFormatsExpect(799, 100)) .givenImageWithDimensionsOf(800, 100, onAllApisAndAllFormatsExpect(800, 100)) .givenImageWithDimensionsOf(801, 100, onAllApisAndAllFormatsExpect(801, 100)) .givenImageWithDimensionsOf(100, 800, onAllApisAndAllFormatsExpect(100, 800)) .givenImageWithDimensionsOf( 87, 78, atAndAbove(KITKAT).with(allFormats().expect(112, 100)), below(KITKAT).with(allFormats().expect(87, 78))) // Upscaling .setTargetDimensions(500, 500) .givenSquareImageWithDimensionOf( 200, atAndAbove(KITKAT).with(allFormats().expect(500, 500)), below(KITKAT).with(allFormats().expect(200, 200))) .givenSquareImageWithDimensionOf( 450, atAndAbove(KITKAT).with(allFormats().expect(500, 500)), below(KITKAT).with(allFormats().expect(450, 450))) .givenImageWithDimensionsOf( 200, 450, atAndAbove(KITKAT).with(allFormats().expect(500, 1125)), below(KITKAT).with(allFormats().expect(200, 450))) // Original scaling .setTargetDimensions(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .givenImageWithDimensionsOf(1821, 2634, onAllApisAndAllFormatsExpect(1821, 2634)) .run(); } @Test public void calculateScaling_withNone() throws IOException { new Tester(DownsampleStrategy.NONE) // See #3673 .setTargetDimensions(1977, 2636) .givenImageWithDimensionsOf(3024, 4032, onAllApisAndAllFormatsExpect(3024, 4032)) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(200, 200)) .givenSquareImageWithDimensionOf(400, onAllApisAndAllFormatsExpect(400, 400)) .givenSquareImageWithDimensionOf(300, onAllApisAndAllFormatsExpect(300, 300)) .givenImageWithDimensionsOf(799, 100, onAllApisAndAllFormatsExpect(799, 100)) .givenImageWithDimensionsOf(800, 100, onAllApisAndAllFormatsExpect(800, 100)) .givenImageWithDimensionsOf(801, 100, onAllApisAndAllFormatsExpect(801, 100)) .givenImageWithDimensionsOf(100, 800, onAllApisAndAllFormatsExpect(100, 800)) .givenImageWithDimensionsOf(87, 78, onAllApisAndAllFormatsExpect(87, 78)) .setTargetDimensions(500, 500) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(200, 200)) .givenSquareImageWithDimensionOf(450, onAllApisAndAllFormatsExpect(450, 450)) .givenImageWithDimensionsOf(200, 450, onAllApisAndAllFormatsExpect(200, 450)) // Original scaling .setTargetDimensions(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .givenImageWithDimensionsOf(1821, 2634, onAllApisAndAllFormatsExpect(1821, 2634)) .run(); } @Test public void calculateScaling_withFitCenter() throws IOException { new Tester(DownsampleStrategy.FIT_CENTER) // See #3673 .setTargetDimensions(1977, 2636) .givenImageWithDimensionsOf( 3024, 4032, atAndAbove(KITKAT).with(allFormats().expect(1977, 2636)), below(KITKAT).with(allFormats().expect(3024, 4032))) .setTargetDimensions(100, 100) .givenSquareImageWithDimensionOf(100, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(200, onAllApisAndAllFormatsExpect(100, 100)) .givenSquareImageWithDimensionOf(400, onAllApisAndAllFormatsExpect(100, 100)) .givenImageWithDimensionsOf( 300, 300, atAndAbove(KITKAT).with(allFormats().expect(100, 100)), below(KITKAT).with(allFormats().expect(150, 150))) .givenImageWithDimensionsOf( 799, 100, atAndAbove(KITKAT).with(allFormats().expect(100, 13)), below(KITKAT).with(formats(JPEG).expect(200, 25), formats(PNG, WEBP).expect(199, 25))) .givenImageWithDimensionsOf( 800, 100, atAndAbove(KITKAT).with(allFormats().expect(100, 13)), below(KITKAT).with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12))) .givenImageWithDimensionsOf( 801, 100, atAndAbove(VERSION_CODES.N) .with(formats(JPEG, WEBP).expect(100, 13), formats(PNG).expect(100, 12)), apis(KITKAT, VERSION_CODES.M) .with(formats(JPEG).expect(100, 13), formats(PNG, WEBP).expect(100, 12)), below(KITKAT) .with( // JPEG is correct because FIT_CENTER wants to give a subsequent transformation // an image that is greater in size than the requested size. On // Api > VERSION_CODES.KITKAT, FIT_CENTER can do the transformation itself. // On < VERSION_CODES.KITKAT, it has to assume a transformation will be run // after it that will fix the rounding error. formats(JPEG).expect(101, 13), formats(PNG, WEBP).expect(100, 12))) .givenImageWithDimensionsOf( 100, 800, atAndAbove(KITKAT).with(allFormats().expect(13, 100)), below(KITKAT).with(formats(JPEG).expect(13, 100), formats(PNG, WEBP).expect(12, 100))) .givenImageWithDimensionsOf( 87, 78, atAndAbove(KITKAT).with(allFormats().expect(100, 90)), below(KITKAT).with(allFormats().expect(87, 78))) .setTargetDimensions(897, 897) .givenImageWithDimensionsOf( 2208, 1520, atAndAbove(KITKAT).with(allFormats().expect(897, 618)), below(KITKAT).with(allFormats().expect(1104, 760))) .setTargetDimensions(270, 270) // This set of larger image examples exercises sample sizes > 8. Android' scaling logic // varies for jpegs. .givenImageWithDimensionsOf( 9014, 1638, // 15 and 16 will OOM so don't run them. atAndAbove(KITKAT).with(allFormats().expect(270, 49)), apis(VERSION_CODES.JELLY_BEAN_MR1, VERSION_CODES.JELLY_BEAN_MR2) .with(allFormats().expect(281, 51))) .givenImageWithDimensionsOf( 1638, 9014, // 15 and 16 will OOM so don't run them. atAndAbove(KITKAT).with(allFormats().expect(49, 270)), apis(VERSION_CODES.JELLY_BEAN_MR1, VERSION_CODES.JELLY_BEAN_MR2) .with(allFormats().expect(51, 281))) .givenImageWithDimensionsOf( 1638, 1638, atAndAbove(KITKAT).with(allFormats().expect(270, 270)), below(KITKAT).with(formats(JPEG).expect(410, 410), formats(PNG, WEBP).expect(409, 409))) .givenImageWithDimensionsOf( 4507, 819, atAndAbove(KITKAT).with(allFormats().expect(270, 49)), below(KITKAT).with(formats(JPEG).expect(282, 51), formats(PNG, WEBP).expect(281, 51))) .givenImageWithDimensionsOf( 4503, 819, atAndAbove(KITKAT).with(allFormats().expect(270, 49)), below(KITKAT).with(allFormats().expect(281, 51))) // Upscaling .setTargetDimensions(500, 500) .givenSquareImageWithDimensionOf( 200, atAndAbove(KITKAT).with(allFormats().expect(500, 500)), below(KITKAT).with(allFormats().expect(200, 200))) .givenSquareImageWithDimensionOf( 450, atAndAbove(KITKAT).with(allFormats().expect(500, 500)), below(KITKAT).with(allFormats().expect(450, 450))) .givenImageWithDimensionsOf( 200, 450, atAndAbove(KITKAT).with(allFormats().expect(222, 500)), below(KITKAT).with(allFormats().expect(200, 450))) // Original scaling .setTargetDimensions(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .givenImageWithDimensionsOf(1821, 2634, onAllApisAndAllFormatsExpect(1821, 2634)) .run(); } /** Returns an error string if the test failed, and {@code null} otherwise. */ @Nullable private static String runScaleTest( CompressFormat format, int initialWidth, int initialHeight, int targetWidth, int targetHeight, int exifOrientation, boolean hasGainmap, boolean allowHardwareConfig, DownsampleStrategy strategy, int expectedWidth, int expectedHeight) throws IOException { Downsampler downsampler = buildDownsampler(); InputStream is = openBitmapStream(format, initialWidth, initialHeight, exifOrientation, hasGainmap); Options options = new Options().set(DownsampleStrategy.OPTION, strategy); options.set(Downsampler.ALLOW_HARDWARE_CONFIG, allowHardwareConfig); Bitmap bitmap; try { bitmap = downsampler.decode(is, targetWidth, targetHeight, options).get(); } catch (OutOfMemoryError e) { return "API: " + Build.VERSION.SDK_INT + ", os: " + Build.VERSION.RELEASE + ", format: " + format + ", strategy: " + strategy + ", orientation: " + exifOrientation + ", allowHardwareConfig: " + allowHardwareConfig + " -" + " Initial " + readableDimens(initialWidth, initialHeight) + " Target " + readableDimens(targetWidth, targetHeight) + " Expected " + readableDimens(expectedWidth, expectedHeight) + " but threw OutOfMemoryError"; } try { if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE && (bitmap.getWidth() != expectedWidth || bitmap.getHeight() != expectedHeight || bitmap.hasGainmap() != hasGainmap)) { return "API: " + Build.VERSION.SDK_INT + ", os: " + Build.VERSION.RELEASE + ", format: " + format + ", strategy: " + strategy + ", orientation: " + exifOrientation + ", hasGainmap: " + hasGainmap + ", allowHardwareConfig: " + allowHardwareConfig + " -" + " Initial " + readableDimens(initialWidth, initialHeight) + " Target " + readableDimens(targetWidth, targetHeight) + " Expected " + readableDimensAndHasGainmap(expectedWidth, expectedHeight, hasGainmap) + ", but Received " + readableDimensAndHasGainmap( bitmap.getWidth(), bitmap.getHeight(), bitmap.hasGainmap()); } else if (bitmap.getWidth() != expectedWidth || bitmap.getHeight() != expectedHeight) { return "API: " + Build.VERSION.SDK_INT + ", os: " + Build.VERSION.RELEASE + ", format: " + format + ", strategy: " + strategy + ", orientation: " + exifOrientation + ", allowHardwareConfig: " + allowHardwareConfig + " -" + " Initial " + readableDimens(initialWidth, initialHeight) + " Target " + readableDimens(targetWidth, targetHeight) + " Expected " + readableDimens(expectedWidth, expectedHeight) + ", but Received " + readableDimens(bitmap.getWidth(), bitmap.getHeight()); } } finally { bitmap.recycle(); } return null; } private static String readableDimens(int width, int height) { return "[" + width + "x" + height + "]"; } private static String readableDimensAndHasGainmap(int width, int height, boolean hasGainmap) { return "[" + width + "x" + height + "], hasGainmap=" + hasGainmap; } private static Downsampler buildDownsampler() { List parsers = Collections.singletonList(new DefaultImageHeaderParser()); DisplayMetrics displayMetrics = new DisplayMetrics(); // XHDPI. displayMetrics.densityDpi = 320; BitmapPool bitmapPool = new BitmapPoolAdapter(); ArrayPool arrayPool = new LruArrayPool(); return new Downsampler(parsers, displayMetrics, bitmapPool, arrayPool); } private static InputStream openBitmapStream( CompressFormat format, int width, int height, int exifOrientation, boolean hasGainmap) { Preconditions.checkArgument( format == CompressFormat.JPEG || exifOrientation == ExifInterface.ORIENTATION_UNDEFINED, "Can only orient JPEGs, but asked for orientation: " + exifOrientation + " with format: " + format); // TODO: support orientations for formats other than JPEG. if (format == CompressFormat.JPEG) { return openFileStream(width, height, exifOrientation, hasGainmap); } else { return openInMemoryStream(format, width, height, hasGainmap); } } private static InputStream openFileStream( int width, int height, int exifOrientation, boolean hasGainmap) { int rotationDegrees = TransformationUtils.getExifOrientationDegrees(exifOrientation); if (rotationDegrees == 270 || rotationDegrees == 90) { int temp = width; width = height; height = temp; } Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); if (hasGainmap && VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { bitmap.setGainmap( // Intentionally not directly imported due to test failures with class resolution when // running on SDK levels < 34. Also, do not extract methods with Gainmap in the method // signature for the same reason. new android.graphics.Gainmap(Bitmap.createBitmap(width / 2, height / 2, Config.ALPHA_8))); } OutputStream os = null; try { File tempFile = File.createTempFile( "ds-" + width + "-" + height + "-" + exifOrientation + "-" + hasGainmap, ".jpeg", ApplicationProvider.getApplicationContext().getCacheDir()); os = new BufferedOutputStream(new FileOutputStream(tempFile)); bitmap.compress(CompressFormat.JPEG, /* quality= */ 100, os); bitmap.recycle(); os.close(); ExifInterface exifInterface = new ExifInterface(tempFile.getAbsolutePath()); exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(exifOrientation)); exifInterface.saveAttributes(); InputStream result = new BufferedInputStream(new FileInputStream(tempFile)); if (!tempFile.delete()) { throw new IllegalStateException("Failed to delete: " + tempFile); } return result; } catch (IOException e) { throw new IllegalStateException(e); } finally { if (os != null) { try { os.close(); } catch (IOException e) { } } } } private static InputStream openInMemoryStream( CompressFormat format, int width, int height, boolean hasGainmap) { Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); if (hasGainmap && VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { // Intentionally not directly imported due to test failures with class resolution when // running on SDK levels < 34. Also, do not extract methods with Gainmap in the method // signature for the same reason. bitmap.setGainmap( new android.graphics.Gainmap(Bitmap.createBitmap(width / 2, height / 2, Config.ALPHA_8))); } ByteArrayOutputStream os = new ByteArrayOutputStream(); bitmap.compress(format, 100 /*quality*/, os); bitmap.recycle(); byte[] data = os.toByteArray(); return new ByteArrayInputStream(data); } static final class Tester { private final DownsampleStrategy strategy; private final List testCases = new ArrayList<>(); private int targetWidth; private int targetHeight; Tester(DownsampleStrategy strategy) { this.strategy = strategy; } Tester setTargetDimensions(int targetWidth, int targetHeight) { this.targetWidth = targetWidth; this.targetHeight = targetHeight; return this; } Tester givenSquareImageWithDimensionOf(int dimension, Api... apis) { return givenImageWithDimensionsOf(dimension, dimension, apis); } Tester givenGainmapImageWithDimensionsOf( int sourceWidth, int sourceHeight, boolean allowHardwareConfig, Api... apis) { testCases.add( new TestCase.Builder() .setSourceWidth(sourceWidth) .setSourceHeight(sourceHeight) .setTargetWidth(targetWidth) .setTargetHeight(targetHeight) .setHasGainmap(true) .setAllowHardwareConfig(allowHardwareConfig) .setApis(apis) .build()); return this; } Tester givenImageWithDimensionsOf(int sourceWidth, int sourceHeight, Api... apis) { testCases.add(new TestCase(sourceWidth, sourceHeight, targetWidth, targetHeight, apis)); return this; } void run() throws IOException { List results = new ArrayList<>(); for (TestCase testCase : testCases) { results.addAll(testCase.test(strategy)); } if (results.isEmpty()) { return; } StringBuilder failure = new StringBuilder("Failing Tests:\n"); for (String result : results) { failure.append(result).append("\n"); } fail(failure.substring(0, failure.length() - 1)); } private static final class TestCase { private final int sourceWidth; private final int sourceHeight; private final int targetWidth; private final int targetHeight; private final boolean hasGainmap; private final boolean allowHardwareConfig; private final Api[] apis; /** * @deprecated Use the {@link Builder}. */ @Deprecated TestCase(int sourceWidth, int sourceHeight, int targetWidth, int targetHeight, Api... apis) { this.sourceWidth = sourceWidth; this.sourceHeight = sourceHeight; this.targetWidth = targetWidth; this.targetHeight = targetHeight; this.hasGainmap = false; this.allowHardwareConfig = false; this.apis = apis; } private TestCase(Builder builder) { this.sourceWidth = builder.sourceWidth; this.sourceHeight = builder.sourceHeight; this.targetWidth = builder.targetWidth; this.targetHeight = builder.targetHeight; this.hasGainmap = builder.hasGainmap; this.allowHardwareConfig = builder.allowHardwareConfig; this.apis = builder.apis; } List test(DownsampleStrategy strategy) throws IOException { List results = new ArrayList<>(); for (Api api : apis) { results.addAll( api.test( sourceWidth, sourceHeight, hasGainmap, allowHardwareConfig, targetWidth, targetHeight, strategy)); } return results; } private static final class Builder { private int sourceWidth; private int sourceHeight; private int targetWidth; private int targetHeight; private boolean hasGainmap; private boolean allowHardwareConfig; @Nullable private Api[] apis; public Builder setSourceWidth(int sourceWidth) { this.sourceWidth = sourceWidth; return this; } public Builder setSourceHeight(int sourceHeight) { this.sourceHeight = sourceHeight; return this; } public Builder setTargetWidth(int targetWidth) { this.targetWidth = targetWidth; return this; } public Builder setTargetHeight(int targetHeight) { this.targetHeight = targetHeight; return this; } public Builder setHasGainmap(boolean hasGainmap) { this.hasGainmap = hasGainmap; return this; } public Builder setAllowHardwareConfig(boolean allowHardwareConfig) { this.allowHardwareConfig = allowHardwareConfig; return this; } public Builder setApis(Api[] apis) { this.apis = apis; return this; } public TestCase build() { Preconditions.checkNotNull(apis); return new TestCase(this); } } } } static final class Api { private final int startVersion; private final int stopVersion; private final Formats[] formats; static Builder apis(int min, int max) { return new Builder().min(min).max(max); } static Builder atAndAbove(int min) { return new Builder().min(min); } static Builder below(int max) { // max is inclusive. return new Builder().max(max - 1); } static Builder allApis() { return new Builder(); } static Api onAllApisAndAllFormatsExpect(int width, int height) { return allApis().with(allFormats().expect(width, height)); } static final class Builder { private int maxVersion = Integer.MAX_VALUE; private int minVersion = Integer.MIN_VALUE; Builder min(int version) { minVersion = version; return this; } Builder max(int version) { this.maxVersion = version; return this; } Api with(Formats... formats) { return new Api(minVersion, maxVersion, formats); } } Api(int startVersion, int stopVersion, Formats... formats) { this.startVersion = startVersion; this.stopVersion = stopVersion; this.formats = formats; } List test( int sourceWidth, int sourceHeight, boolean hasGainmap, boolean allowHardwareConfig, int targetWidth, int targetHeight, DownsampleStrategy strategy) throws IOException { if (Build.VERSION.SDK_INT < startVersion || Build.VERSION.SDK_INT > stopVersion) { return Collections.emptyList(); } List results = new ArrayList<>(); for (Formats format : formats) { results.addAll( format.runTest( sourceWidth, sourceHeight, hasGainmap, allowHardwareConfig, targetWidth, targetHeight, strategy)); } return results; } } static final class Formats { private final int expectedWidth; private final int expectedHeight; private final CompressFormat[] formats; private static final int[] ALL_EXIF_ORIENTATIONS = new int[] { ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_TRANSPOSE, ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_ROTATE_270 }; private static final int[] UNDEFINED_EXIF_ORIENTATIONS = new int[] {ExifInterface.ORIENTATION_UNDEFINED}; static final class Builder { private final CompressFormat[] formats; static Builder allFormats() { return formats(CompressFormat.values()); } static Builder formats(CompressFormat... formats) { return new Builder(formats); } Builder(CompressFormat... formats) { this.formats = formats; } Formats expect(int width, int height) { return new Formats(formats, width, height); } } Formats(CompressFormat[] formats, int expectedWidth, int expectedHeight) { this.formats = formats; this.expectedWidth = expectedWidth; this.expectedHeight = expectedHeight; } List runTest( int sourceWidth, int sourceHeight, boolean hasGainmap, boolean allowHardwareConfig, int targetWidth, int targetHeight, DownsampleStrategy strategy) throws IOException { List result = new ArrayList<>(); for (CompressFormat format : formats) { int[] exifOrientations = format == CompressFormat.JPEG ? ALL_EXIF_ORIENTATIONS : UNDEFINED_EXIF_ORIENTATIONS; for (int exifOrientation : exifOrientations) { String testResult = runScaleTest( format, sourceWidth, sourceHeight, targetWidth, targetHeight, exifOrientation, hasGainmap, allowHardwareConfig, strategy, expectedWidth, expectedHeight); if (testResult != null) { result.add(testResult); } } } return result; } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/load/resource/gif/GifDrawableTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.google.common.truth.Truth.assertThat; import android.Manifest.permission; import android.content.Context; import android.os.Build; import android.view.View; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.GrantPermissionRule; import com.bumptech.glide.load.resource.gif.GifDrawable.GifState; import com.bumptech.glide.load.resource.gif.GifFrameLoader.OnEveryFrameListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.test.GlideApp; import com.bumptech.glide.test.ResourceIds; import com.bumptech.glide.testutil.ConcurrencyHelper; import com.bumptech.glide.testutil.TearDownGlide; import com.bumptech.glide.util.Preconditions; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class GifDrawableTest { @Rule public final TestName testName = new TestName(); @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); @Rule public final GrantPermissionRule grantPermissionRule; private final ConcurrencyHelper concurrencyHelper = new ConcurrencyHelper(); { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { grantPermissionRule = GrantPermissionRule.grant(permission.SYSTEM_ALERT_WINDOW); } else { grantPermissionRule = GrantPermissionRule.grant(); } } private Context context; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); } @Test public void loadGif_withInterlacedTransparentGif_sizeOriginal_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context) .asGif() .load(ResourceIds.raw.interlaced_transparent_gif) .submit()); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_withInterlacedTransparentGif_downsampled_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context) .asGif() .load(ResourceIds.raw.interlaced_transparent_gif) .submit(10, 10)); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_withTransparentGif_sizeOriginal_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context).asGif().load(ResourceIds.raw.transparent_gif).submit()); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_withTransparentGif_downsampled_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context).asGif().load(ResourceIds.raw.transparent_gif).submit(10, 10)); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_withOpaqueGif_sizeOriginal_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context).asGif().load(ResourceIds.raw.opaque_gif).submit()); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_withOpaqueGif_downsampled_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context).asGif().load(ResourceIds.raw.opaque_gif).submit(10, 10)); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_withOpaqueInterlacedGif_sizeOriginal_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context).asGif().load(ResourceIds.raw.opaque_interlaced_gif).submit()); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_withOpaqueInterlacedGif_downsampled_succeeds() throws ExecutionException, InterruptedException { GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context) .asGif() .load(ResourceIds.raw.opaque_interlaced_gif) .submit(10, 10)); assertThat(gifDrawable).isNotNull(); } @Test public void loadGif_intoImageView_afterStop_restartsGif() throws ExecutionException, InterruptedException { // Mimic the state the Drawable can get into if it was loaded into a View previously and stopped // so that it ended up with a pending frame that finished after the stop call. final GifDrawable gifDrawable = concurrencyHelper.get( GlideApp.with(context) .asGif() .load(ResourceIds.raw.dl_world_anim) .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)); final CountDownLatch waitForGifFrame = new CountDownLatch(1); // Starting/Stopping loads in GIFs must happen on the main thread. concurrencyHelper.runOnMainThread( new Runnable() { @Override public void run() { // Make sure a frame is loaded while the drawable is stopped. GifState gifState = (GifState) Preconditions.checkNotNull(gifDrawable.getConstantState()); gifState.frameLoader.setOnEveryFrameReadyListener( new OnEveryFrameListener() { @Override public void onFrameReady() { waitForGifFrame.countDown(); } }); gifDrawable.start(); gifDrawable.stop(); } }); ConcurrencyHelper.waitOnLatch(waitForGifFrame); // Load the Drawable with the pending frame into a new View and make sure it ends up in the // running state. final ImageView imageView = new ImageView(context); concurrencyHelper.runOnMainThread( new Runnable() { @Override public void run() { addViewToWindow(imageView); } }); concurrencyHelper.loadOnMainThread( GlideApp.with(context).load(gifDrawable).override(Target.SIZE_ORIGINAL), imageView); GifDrawable drawableFromView = (GifDrawable) imageView.getDrawable(); assertThat(drawableFromView.isRunning()).isTrue(); drawableFromView.stop(); gifDrawable.stop(); } @SuppressWarnings("deprecation") private void addViewToWindow(View view) { WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT; layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; layoutParams.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? LayoutParams.TYPE_APPLICATION_OVERLAY : Build.VERSION.SDK_INT == Build.VERSION_CODES.M ? LayoutParams.TYPE_TOAST : LayoutParams.TYPE_SYSTEM_ALERT; WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Preconditions.checkNotNull(windowManager).addView(view, layoutParams); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/test/BitmapRegressionTester.java ================================================ package com.bumptech.glide.test; import static com.bumptech.glide.testutil.BitmapSubject.assertThat; import static org.junit.Assume.assumeTrue; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Environment; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.RequestBuilder; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Method; import java.util.Arrays; import java.util.concurrent.ExecutionException; import org.junit.rules.TestName; /** * Checks for regressions for a given Glide load by comparing the result of a load to a previously * saved Bitmap. * *

Can be used to generate or re-generate expected {@link Bitmap}s by placing a file named * "regenerate" in /sdcard/DCIM/test_files. The apks containing this tester will need to have {@link * android.Manifest.permission#WRITE_EXTERNAL_STORAGE}. Resources can be split by apk by adding * {@link SplitBySdk} to test methods or classes. If {@link SplitBySdk} is added to both a test * class and a particular method, the values from the method will be used. * *

This class only handles exactly one Bitmap comparison per test method because the resource * names it expects and generates are based on the method name. */ public final class BitmapRegressionTester { private static final String RESOURCE_TYPE = "raw"; private static final String EXTENSION = ".png"; private static final String REGENERATE_SIGNAL_FILE_NAME = "regenerate"; private static final String GENERATED_FILES_DIR = "test_files"; private static final String SEPARATOR = "_"; private static final int RESOURCE_ID_NOT_FOUND = 0; private final Class testClass; private final TestName testName; private final Context context = ApplicationProvider.getApplicationContext(); public static AssumeCanRun newInstance(Class testClass, TestName testName) { return new AssumeCanRun(new BitmapRegressionTester(testClass, testName)); } private BitmapRegressionTester(Class testClass, TestName testName) { this.testClass = testClass; this.testName = testName; if (testClass.getAnnotation(RegressionTest.class) == null) { throw new IllegalArgumentException( testClass + " must be annotated with " + RegressionTest.class); } } public static final class AssumeCanRun { private final BitmapRegressionTester regressionTester; private AssumeCanRun(BitmapRegressionTester regressionTester) { this.regressionTester = regressionTester; } public BitmapRegressionTester assumeShouldRun() { boolean shouldRun = regressionTester.shouldRun(); assumeTrue(shouldRun); return regressionTester; } } public Bitmap test(RequestBuilder request) throws ExecutionException, InterruptedException { Bitmap result = request.submit().get(); if (writeNewExpected()) { writeBitmap(result); } Bitmap expected = decodeExpected(); assertThat(result).sameAs(expected); return result; } private String getResourceName() { return getClassNameString() + SEPARATOR + testName.getMethodName().toLowerCase() + getSdkIntString() + getCpuString(); } private String getClassNameString() { StringBuilder result = new StringBuilder(); for (char c : testClass.getSimpleName().toCharArray()) { if (Character.isUpperCase(c)) { result.append(Character.toLowerCase(c)); } } return result.toString(); } @Nullable private SplitBySdk getSplitBySdkValues() { SplitBySdk result; try { Method method = testClass.getMethod(testName.getMethodName(), /* parameterTypes...= */ (Class[]) null); result = method.getAnnotation(SplitBySdk.class); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } if (result == null) { result = testClass.getAnnotation(SplitBySdk.class); } return result; } private String getCpuString() { return splitByCpu() ? SEPARATOR + Build.CPU_ABI.replace("-", "_") : ""; } private boolean splitByCpu() { return testClass.getAnnotation(SplitByCpu.class) != null; } private String getSdkIntString() { SplitBySdk splitBySdk = getSplitBySdkValues(); if (splitBySdk == null) { return ""; } int targetSdk = -1; int[] values = splitBySdk.value(); Arrays.sort(values); for (int value : values) { if (value > Build.VERSION.SDK_INT) { break; } targetSdk = value; } if (targetSdk == -1) { return ""; } return SEPARATOR + targetSdk; } private File getTestFilesDir() { File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); return new File(dir, GENERATED_FILES_DIR); } private void writeBitmap(Bitmap bitmap) { File testFilesDir = getTestFilesDir(); File subdirectory = new File(testFilesDir, RESOURCE_TYPE); if (!subdirectory.exists() && !subdirectory.mkdirs()) { throw new IllegalArgumentException("Failed to make directory: " + subdirectory); } File file = new File(subdirectory, getResourceName() + EXTENSION); if (file.exists() && !file.delete()) { throw new IllegalStateException("Failed to remove existing file: " + file); } OutputStream os = null; try { os = new BufferedOutputStream(new FileOutputStream(file)); bitmap.compress(CompressFormat.PNG, /* quality= */ 100, os); os.close(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (os != null) { try { os.close(); } catch (IOException e) { // Ignored. } } } } private boolean shouldRun() { return writeNewExpected() || getResourceId() != RESOURCE_ID_NOT_FOUND; } private boolean writeNewExpected() { File testFiles = getTestFilesDir(); return new File(testFiles, REGENERATE_SIGNAL_FILE_NAME).exists(); } private int getResourceId() { return context .getResources() .getIdentifier(getResourceName(), RESOURCE_TYPE, context.getPackageName()); } private Bitmap decodeExpected() { int resourceId = getResourceId(); if (resourceId == RESOURCE_ID_NOT_FOUND) { throw new IllegalArgumentException( "Failed to find resource for: " + getResourceName() + " with type: " + RESOURCE_TYPE + " and package: " + context.getPackageName()); } BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; return BitmapFactory.decodeResource(context.getResources(), resourceId, options); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/test/CanonicalBitmap.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.util.Preconditions; public final class CanonicalBitmap { @Nullable private Bitmap bitmap; @Nullable private Float scaleFactor; @NonNull public synchronized Bitmap getBitmap() { if (bitmap == null) { bitmap = decodeBitmap(); } return bitmap; } public CanonicalBitmap scale(float scaleFactor) { Preconditions.checkArgument(bitmap == null, "Can't set scale factor after decoding image"); this.scaleFactor = scaleFactor; return this; } public int getWidth() { return getBitmap().getWidth(); } public int getHeight() { return getBitmap().getHeight(); } private Bitmap decodeBitmap() { Context context = ApplicationProvider.getApplicationContext(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; int resourceId = ResourceIds.raw.canonical; Bitmap result = BitmapFactory.decodeResource(context.getResources(), resourceId, options); if (scaleFactor != null) { result = Bitmap.createScaledBitmap( result, (int) (result.getWidth() * scaleFactor), (int) (result.getHeight() * scaleFactor), /* filter= */ false); } // Make sure the Bitmap is immutable. return result; } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/test/ModelGeneratorRule.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import androidx.annotation.RawRes; import androidx.test.core.app.ApplicationProvider; import com.google.common.io.ByteStreams; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.atomic.AtomicInteger; import org.junit.rules.ExternalResource; /** Converts raw resources into specific model types (Uris, Files, byte arrays etc). */ public final class ModelGeneratorRule extends ExternalResource { private static final String TEMP_FOLDER_NAME = "model_generator_rule_cache"; private final Context context = ApplicationProvider.getApplicationContext(); private final AtomicInteger fileNameCounter = new AtomicInteger(); private File getTempDir() { File tempDir = new File(context.getCacheDir(), TEMP_FOLDER_NAME); if (!tempDir.mkdirs() && (!tempDir.exists() || !tempDir.isDirectory())) { throw new IllegalStateException("Failed to mkdirs for: " + tempDir); } return tempDir; } private File nextTempFile() { String name = "model_generator" + fileNameCounter.getAndIncrement(); return new File(getTempDir(), name); } public File asFile(@RawRes int resourceId) throws IOException { return writeToFile(resourceId); } public byte[] asByteArray(@RawRes int resourceId) throws IOException { Resources resources = context.getResources(); InputStream is = resources.openRawResource(resourceId); return ByteStreams.toByteArray(is); } private File writeToFile(@RawRes int resourceId) throws IOException { byte[] data = asByteArray(resourceId); File result = nextTempFile(); try (OutputStream os = new FileOutputStream(result)) { os.write(data); } return result; } @Override protected void after() { super.after(); cleanupTempDir(); } private void cleanupTempDir() { File tempDir = getTempDir(); File[] children = tempDir.listFiles(); if (children != null) { for (File child : children) { if (child.isDirectory()) { throw new IllegalStateException("Expected a file, but was a directory: " + child); } if (!child.delete()) { throw new IllegalStateException("Failed to delete: " + child); } } } if (!tempDir.delete()) { throw new IllegalStateException("Failed to delete temp dir: " + tempDir); } } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/test/RegressionTest.java ================================================ package com.bumptech.glide.test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicates that a test is a regression test that relies on comparing a newly transformed image to * a previously generated copy of the same image to detect changes. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface RegressionTest { // Intentionally empty. } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/test/ResourceIds.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.res.Resources; import androidx.test.core.app.ApplicationProvider; /** * Internally in google we don't appear to be able to reference resource ids directly, this class is * a hack around that until we figure out what's going wrong. */ public final class ResourceIds { private ResourceIds() { // Utility class. } public interface raw { int dl_world_anim = getResourceId("raw", "dl_world_anim"); int canonical = getResourceId("raw", "canonical"); int canonical_large = getResourceId("raw", "canonical_large"); int canonical_png = getResourceId("raw", "canonical_png"); int canonical_transparent_png = getResourceId("raw", "canonical_transparent_png"); int interlaced_transparent_gif = getResourceId("raw", "interlaced_transparent_gif"); int transparent_gif = getResourceId("raw", "transparent_gif"); int opaque_gif = getResourceId("raw", "opaque_gif"); int opaque_interlaced_gif = getResourceId("raw", "opaque_interlaced_gif"); int webkit_logo_p3 = getResourceId("raw", "webkit_logo_p3"); int video = getResourceId("raw", "video"); int animated_webp = getResourceId("raw", "dl_world_anim_webp"); int animated_avif = getResourceId("raw", "dl_world_anim_avif"); } public interface drawable { int bitmap_alias = getResourceId("drawable", "bitmap_alias"); int googlelogo_color_120x44dp = getResourceId("drawable", "googlelogo_color_120x44dp"); int shape_drawable = getResourceId("drawable", "shape_drawable"); int state_list_drawable = getResourceId("drawable", "state_list_drawable"); int vector_drawable = getResourceId("drawable", "vector_drawable"); } private static int getResourceId(String type, String resourceName) { Context context = ApplicationProvider.getApplicationContext(); Resources res = context.getResources(); return res.getIdentifier(resourceName, type, context.getPackageName()); } } ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/test/SplitByCpu.java ================================================ package com.bumptech.glide.test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicates that the test relies on transformations or operations that may produce different * outputs on different CPUs. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface SplitByCpu {} ================================================ FILE: instrumentation/src/androidTest/java/com/bumptech/glide/test/SplitBySdk.java ================================================ package com.bumptech.glide.test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Used by {@link BitmapRegressionTester} to generate SDK specific resources to account for * differences in Android's image decoding APIs across versions. */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface SplitBySdk { int[] value(); } ================================================ FILE: instrumentation/src/main/AndroidManifest.xml ================================================ ================================================ FILE: instrumentation/src/main/java/com/bumptech/glide/test/DefaultFragmentActivity.java ================================================ package com.bumptech.glide.test; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import com.bumptech.glide.instrumentation.R; public class DefaultFragmentActivity extends FragmentActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.default_fragment_activity); } } ================================================ FILE: instrumentation/src/main/java/com/bumptech/glide/test/ForceDarkOrLightModeActivity.java ================================================ package com.bumptech.glide.test; import android.content.Context; import android.content.Intent; import android.os.Build.VERSION_CODES; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import com.bumptech.glide.instrumentation.R; import com.bumptech.glide.util.Preconditions; public class ForceDarkOrLightModeActivity extends AppCompatActivity { private static final int INVALID_MODE = -1; private static final String ARGS_NIGHT_MODE = "args_night_mode"; public static Intent forceLightMode(Context context) { return newArgs(context, AppCompatDelegate.MODE_NIGHT_NO); } public static Intent forceDarkMode(Context context) { return newArgs(context, AppCompatDelegate.MODE_NIGHT_YES); } private static Intent newArgs(Context context, int nightMode) { Intent intent = new Intent(context, ForceDarkOrLightModeActivity.class); intent.putExtra(ARGS_NIGHT_MODE, nightMode); return intent; } @RequiresApi(api = VERSION_CODES.JELLY_BEAN_MR1) @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); int modeToForce = getIntent().getExtras().getInt(ARGS_NIGHT_MODE, INVALID_MODE); Preconditions.checkArgument(modeToForce != INVALID_MODE, "Invalid mode: " + modeToForce); getDelegate().setLocalNightMode(modeToForce); setContentView(R.layout.default_fragment_activity); } } ================================================ FILE: instrumentation/src/main/java/com/bumptech/glide/test/GlideWithAsDifferentSupertypesActivity.java ================================================ package com.bumptech.glide.test; import android.app.Activity; import android.content.Context; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import com.bumptech.glide.Glide; public class GlideWithAsDifferentSupertypesActivity extends FragmentActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Glide.with(this); Glide.with((Context) this); Glide.with((Activity) this); } } ================================================ FILE: instrumentation/src/main/java/com/bumptech/glide/test/GlideWithBeforeSuperOnCreateActivity.java ================================================ package com.bumptech.glide.test; import android.os.Bundle; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import com.bumptech.glide.Glide; public class GlideWithBeforeSuperOnCreateActivity extends FragmentActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { Glide.with(this); super.onCreate(savedInstanceState); setContentView(new TextView(this)); } @Override protected void onResume() { super.onResume(); Glide.with(this); } } ================================================ FILE: instrumentation/src/main/java/com/bumptech/glide/test/InstrumentationAppGlideModule.java ================================================ package com.bumptech.glide.test; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class InstrumentationAppGlideModule extends AppGlideModule { // Intentionally empty. } ================================================ FILE: instrumentation/src/main/res/drawable/bitmap_alias.xml ================================================ ================================================ FILE: instrumentation/src/main/res/drawable/shape_drawable.xml ================================================ ================================================ FILE: instrumentation/src/main/res/drawable/state_list_drawable.xml ================================================ ================================================ FILE: instrumentation/src/main/res/drawable/vector_drawable.xml ================================================ ================================================ FILE: instrumentation/src/main/res/drawable/vector_drawable_dark.xml ================================================ ================================================ FILE: instrumentation/src/main/res/drawable/vector_drawable_light.xml ================================================ ================================================ FILE: instrumentation/src/main/res/layout/default_fragment_activity.xml ================================================ ================================================ FILE: instrumentation/src/main/res/values/colors.xml ================================================ #f9b840 ================================================ FILE: instrumentation/src/main/res/values/strings.xml ================================================ ================================================ FILE: instrumentation/src/main/res/values/styles.xml ================================================ ================================================ FILE: instrumentation/src/main/res/values-night/colors.xml ================================================ #048b9f ================================================ FILE: integration/avif/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.avif" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) implementation(libs.avif) implementation(libs.guava) annotationProcessor(project(":annotation:compiler")) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/avif/gradle.properties ================================================ POM_NAME=Glide AVIF Integration POM_ARTIFACT_ID=avif-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library to support AVIF images in Glide ================================================ FILE: integration/avif/lint.xml ================================================ ================================================ FILE: integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifByteBufferBitmapDecoder.java ================================================ package com.bumptech.glide.integration.avif; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.util.Log; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.BitmapResource; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.util.Preconditions; import java.nio.ByteBuffer; import javax.annotation.Nullable; import org.aomedia.avif.android.AvifDecoder; import org.aomedia.avif.android.AvifDecoder.Info; /** A Glide {@link ResourceDecoder} capable of decoding Avif images. */ public final class AvifByteBufferBitmapDecoder implements ResourceDecoder { private static final String TAG = "AvifBitmapDecoder"; private final BitmapPool bitmapPool; public AvifByteBufferBitmapDecoder(BitmapPool bitmapPool) { this.bitmapPool = Preconditions.checkNotNull(bitmapPool); } private ByteBuffer maybeCopyBuffer(ByteBuffer source) { // Native calls can only access ByteBuffer if isDirect() is true. Otherwise, we would have to // make a copy into a direct ByteBuffer. if (source.isDirect()) { return source; } ByteBuffer sourceCopy = ByteBuffer.allocateDirect(source.remaining()); sourceCopy.put(source); sourceCopy.flip(); return sourceCopy; } @Override @Nullable public Resource decode(ByteBuffer source, int width, int height, Options options) { ByteBuffer sourceCopy = maybeCopyBuffer(source); Info info = new Info(); if (!AvifDecoder.getInfo(sourceCopy, sourceCopy.remaining(), info)) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Requested to decode byte buffer which cannot be handled by AvifDecoder"); } return null; } Bitmap.Config config; if (options.get(Downsampler.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565) { config = Config.RGB_565; } else { config = (info.depth == 8) ? Config.ARGB_8888 : Config.RGBA_F16; } Bitmap bitmap = bitmapPool.get(info.width, info.height, config); if (!AvifDecoder.decode(sourceCopy, sourceCopy.remaining(), bitmap)) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Failed to decode ByteBuffer as Avif."); } bitmapPool.put(bitmap); return null; } return BitmapResource.obtain(bitmap, bitmapPool); } @Override public boolean handles(ByteBuffer source, Options options) { return AvifDecoder.isAvifImage(maybeCopyBuffer(source)); } } ================================================ FILE: integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifGlideModule.java ================================================ package com.bumptech.glide.integration.avif; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder; import com.bumptech.glide.module.LibraryGlideModule; import java.io.InputStream; import java.nio.ByteBuffer; /** Glide support for AVIF Images. */ @GlideModule public final class AvifGlideModule extends LibraryGlideModule { @Override public void registerComponents( @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { // Add the Avif ResourceDecoders before any of the available system decoders. This ensures that // the integration will be preferred for Avif images. AvifByteBufferBitmapDecoder byteBufferBitmapDecoder = new AvifByteBufferBitmapDecoder(glide.getBitmapPool()); registry.prepend( Registry.BUCKET_BITMAP, ByteBuffer.class, Bitmap.class, byteBufferBitmapDecoder); registry.prepend( Registry.BUCKET_BITMAP_DRAWABLE, ByteBuffer.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(context.getResources(), byteBufferBitmapDecoder)); AvifStreamBitmapDecoder streamBitmapDecoder = new AvifStreamBitmapDecoder( registry.getImageHeaderParsers(), byteBufferBitmapDecoder, glide.getArrayPool()); registry.prepend(Registry.BUCKET_BITMAP, InputStream.class, Bitmap.class, streamBitmapDecoder); registry.prepend( Registry.BUCKET_BITMAP_DRAWABLE, InputStream.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(context.getResources(), streamBitmapDecoder)); } } ================================================ FILE: integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifStreamBitmapDecoder.java ================================================ package com.bumptech.glide.integration.avif; import android.graphics.Bitmap; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.util.ByteBufferUtil; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import java.io.InputStream; import java.util.List; import javax.annotation.Nullable; /** A Glide {@link ResourceDecoder} capable of decoding Avif Images. */ public final class AvifStreamBitmapDecoder implements ResourceDecoder { private static final String TAG = "AvifStreamBitmapDecoder"; private final List parsers; private final AvifByteBufferBitmapDecoder avifByteBufferDecoder; private final ArrayPool arrayPool; public AvifStreamBitmapDecoder( List parsers, AvifByteBufferBitmapDecoder avifByteBufferDecoder, ArrayPool arrayPool) { this.parsers = parsers; this.avifByteBufferDecoder = Preconditions.checkNotNull(avifByteBufferDecoder); this.arrayPool = Preconditions.checkNotNull(arrayPool); } @Override @Nullable public Resource decode(InputStream source, int width, int height, Options options) throws IOException { return avifByteBufferDecoder.decode(ByteBufferUtil.fromStream(source), width, height, options); } @Override public boolean handles(InputStream source, Options options) throws IOException { ImageType type = ImageHeaderParserUtils.getType(parsers, source, arrayPool); return type.equals(ImageType.AVIF) || type.equals(ImageType.ANIMATED_AVIF); } } ================================================ FILE: integration/build.gradle.kts ================================================ // keep an empty file to make sure Gradle recognizes the properties ================================================ FILE: integration/compose/api/compose.api ================================================ public abstract interface annotation class com/bumptech/glide/integration/compose/ExperimentalGlideComposeApi : java/lang/annotation/Annotation { } public final class com/bumptech/glide/integration/compose/GlideImageKt { public static final fun GlideImage (Ljava/lang/Object;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/ColorFilter;Lcom/bumptech/glide/integration/compose/Placeholder;Lcom/bumptech/glide/integration/compose/Placeholder;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun placeholder (I)Lcom/bumptech/glide/integration/compose/Placeholder; public static final fun placeholder (Landroid/graphics/drawable/Drawable;)Lcom/bumptech/glide/integration/compose/Placeholder; public static final fun placeholder (Lkotlin/jvm/functions/Function2;)Lcom/bumptech/glide/integration/compose/Placeholder; } public abstract interface class com/bumptech/glide/integration/compose/GlidePreloadingData { public abstract fun get (ILandroidx/compose/runtime/Composer;I)Lkotlin/Pair; public abstract fun getSize ()I } public abstract class com/bumptech/glide/integration/compose/Placeholder { public static final field $stable I } public final class com/bumptech/glide/integration/compose/PreloadKt { public static final fun rememberGlidePreloadingData-Z8o_i8w (Ljava/util/List;JILjava/lang/Integer;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)Lcom/bumptech/glide/integration/compose/GlidePreloadingData; public static final fun rememberGlidePreloadingData-u6VnWhU (ILkotlin/jvm/functions/Function1;JILjava/lang/Integer;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)Lcom/bumptech/glide/integration/compose/GlidePreloadingData; } ================================================ FILE: integration/compose/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("com.android.library") id("org.jetbrains.kotlin.android") } android { namespace = "com.bumptech.glide.integration.compose" compileSdk = 34 defaultConfig { minSdk = 21 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildFeatures { compose = true } buildTypes { getByName("release") { isMinifyEnabled = false } } composeOptions { kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) } } testOptions { unitTests { isIncludeAndroidResources = true } } } tasks.withType().configureEach { if (!name.contains("Test")) { kotlinOptions.freeCompilerArgs += "-Xexplicit-api=strict" } } dependencies { implementation(project(":library")) implementation(project(":integration:ktx")) implementation(project(":integration:recyclerview")) { isTransitive = false } implementation(libs.compose.foundation) implementation(libs.compose.ui) implementation(libs.drawablepainter) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.compose) debugImplementation(libs.compose.ui.testmanifest) testImplementation(libs.compose.ui.testmanifest) testImplementation(libs.compose.ui.testjunit4) testImplementation(libs.junit) testImplementation(libs.robolectric) testImplementation(libs.androidx.appcompat) testImplementation(libs.androidx.junit) testImplementation(libs.androidx.test.runner) testImplementation(libs.androidx.lifecycle.runtime.testing) androidTestImplementation(libs.junit) androidTestImplementation(libs.compose.ui.testjunit4) androidTestImplementation(libs.androidx.espresso) androidTestImplementation(libs.androidx.espresso.idling) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.compose.material) androidTestImplementation(libs.truth) androidTestImplementation(project(":testutil")) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/compose/gradle.properties ================================================ POM_NAME=Glide Compose Integration POM_ARTIFACT_ID=compose POM_PACKAGING=aar POM_DESCRIPTION=An integration library to integrate with Jetpack Compose VERSION_MAJOR=1 VERSION_MINOR=0 VERSION_PATCH=0 VERSION_NAME=1.0.0-beta08 ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageCustomDrawableTransformationTest.kt ================================================ @file:OptIn(ExperimentalGlideComposeApi::class, ExperimentalCoroutinesApi::class) package com.bumptech.glide.integration.compose import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ScaleFactor import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner import com.bumptech.glide.integration.compose.test.Constants import com.bumptech.glide.integration.compose.test.GlideComposeRule import com.bumptech.glide.integration.compose.test.assertDisplaysInstance import com.bumptech.glide.integration.compose.test.onNodeWithDefaultContentDescription import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized /** * Tests Issue #4943. * * Transformable types are tested in [GlideImageDefaultTransformationTest]. */ @RunWith(Parameterized::class) class GlideImageCustomDrawableTransformationTest( private val contentScale: ContentScale, // We need a shorter test name than the ContentScale class name to make google3 happy, so we // add an extra parameter. Unfortunately that means we need to list it in the constructor even // though it's only used by Parameters to create the test name. @Suppress("unused") private val name: String, ) { @get:Rule val glideComposeRule = GlideComposeRule() @Test fun glideImage_nonBitmapDrawable_doesNotThrow() = runTest { val customDrawable = FakeDrawable() glideComposeRule.setContent { GlideImageWithCustomDrawable(customDrawable) } glideComposeRule .onNodeWithDefaultContentDescription() .assertDisplaysInstance(customDrawable) } @Test fun glideImage_animatableDrawable_doesNotThrow() = runTest { val customDrawable = FakeAnimatableDrawable() glideComposeRule.setContent { GlideImageWithCustomDrawable(customDrawable) } glideComposeRule .onNodeWithDefaultContentDescription() .assertDisplaysInstance(customDrawable) } @Test fun glideImage_animatableDrawable_stopsAnimationWhenLifecycleNotStarted() = runTest { val customDrawable = FakeAnimatableDrawable() val testLifecycleOwner = TestLifecycleOwner(initialState = Lifecycle.State.STARTED) glideComposeRule.setContent { CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) { GlideImageWithCustomDrawable(customDrawable) } } assertThat(customDrawable.animating).isTrue() testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP) assertThat(customDrawable.animating).isFalse() } @Composable private fun GlideImageWithCustomDrawable(customDrawable: FakeDrawable) { GlideImage( model = customDrawable, contentScale = contentScale, contentDescription = Constants.DEFAULT_DESCRIPTION, modifier = Modifier.size(200.dp, 100.dp), ) } companion object { // Add a second parameter purely to make the test name shorter, see the comment on the test // constructor argument for details. @JvmStatic @Parameterized.Parameters(name = "{1}") fun data() = arrayOf( arrayOf(ContentScale.Crop, "Crop"), arrayOf(ContentScale.FillBounds, "FillBounds"), arrayOf(ContentScale.FillHeight, "FillHeight"), arrayOf(ContentScale.FillWidth, "FillWidth"), arrayOf(ContentScale.Fit, "Fit"), arrayOf(ContentScale.Inside, "Inside"), arrayOf(ContentScale.None, "None"), arrayOf( object : ContentScale { override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor = ContentScale.Fit.computeScaleFactor(srcSize, dstSize) }, "Custom", ), ) } } @Suppress("DeprecatedCallableAddReplaceWith") private open class FakeDrawable : Drawable() { override fun draw(p0: Canvas) {} override fun setAlpha(p0: Int) = throw UnsupportedOperationException() override fun setColorFilter(p0: ColorFilter?) = throw UnsupportedOperationException() @Deprecated("Deprecated in Java") override fun getOpacity(): Int = throw UnsupportedOperationException() } private class FakeAnimatableDrawable : FakeDrawable(), Animatable { var animating: Boolean? = null override fun start() { animating = true } override fun stop() { animating = false } override fun isRunning(): Boolean = throw UnsupportedOperationException() } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageDefaultTransformationTest.kt ================================================ @file:OptIn( ExperimentalCoroutinesApi::class, ExperimentGlideFlows::class, ExperimentalGlideComposeApi::class, ) package com.bumptech.glide.integration.compose import android.content.Context import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.test.core.app.ApplicationProvider import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.integration.compose.test.Constants import com.bumptech.glide.integration.compose.test.GlideComposeRule import com.bumptech.glide.integration.compose.test.assertDisplays import com.bumptech.glide.integration.compose.test.dpToPixels import com.bumptech.glide.integration.compose.test.onNodeWithDefaultContentDescription import com.bumptech.glide.integration.ktx.ExperimentGlideFlows import com.bumptech.glide.integration.ktx.Resource import com.bumptech.glide.integration.ktx.Status import com.bumptech.glide.integration.ktx.flow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test /** Non-transformable types are tested in [GlideImageCustomDrawableTransformationTest] */ class GlideImageDefaultTransformationTest { private val context: Context = ApplicationProvider.getApplicationContext() @get:Rule val glideComposeRule = GlideComposeRule() @Test fun glideImage_withContentScaleNone_noTransformation_doesNotApplyTransformation() = runTest { val resourceId = android.R.drawable.star_big_on val expectedDrawable = loadExpectedDrawable(resourceId) glideComposeRule.setContent { ContentScaleGlideImage(model = resourceId, contentScale = ContentScale.None) } glideComposeRule.onNodeWithDefaultContentDescription().assertDisplays(expectedDrawable) } @Test fun glideImage_withContentScaleFit_noTransformation_appliesCenterInsideTransformation() = runTest { val resourceId = android.R.drawable.star_big_on val expectedDrawable = loadExpectedDrawable(resourceId) { it.centerInside() } glideComposeRule.setContent { ContentScaleGlideImage(model = resourceId, contentScale = ContentScale.Fit) } glideComposeRule.onNodeWithDefaultContentDescription().assertDisplays(expectedDrawable) } @Test fun glideImage_withContentScaleFit_explicitTransformation_usesExplicitTransformation() = runTest { val resourceId = android.R.drawable.star_big_on val expectedDrawable = loadExpectedDrawable(resourceId) { it.centerCrop() } glideComposeRule.setContent { ContentScaleGlideImage(model = resourceId, contentScale = ContentScale.Fit) { it.centerCrop() } } glideComposeRule.onNodeWithDefaultContentDescription().assertDisplays(expectedDrawable) } @Test fun glideImage_withContentScaleInside_noTransformation_appliesCenterInsideTransformation() = runTest { val resourceId = android.R.drawable.star_big_on val expectedDrawable = loadExpectedDrawable(resourceId) { it.centerInside() } glideComposeRule.setContent { ContentScaleGlideImage(model = resourceId, contentScale = ContentScale.Inside) } glideComposeRule.onNodeWithDefaultContentDescription().assertDisplays(expectedDrawable) } @Test fun glideImage_withContentScaleInside_explicitTransformation_usesExplicitTransformation() = runTest { val resourceId = android.R.drawable.star_big_on val expectedDrawable = loadExpectedDrawable(resourceId) { it.centerCrop() } glideComposeRule.setContent { ContentScaleGlideImage(model = resourceId, contentScale = ContentScale.Inside) { it.centerCrop() } } glideComposeRule.onNodeWithDefaultContentDescription().assertDisplays(expectedDrawable) } @Test fun glideImage_withContentScaleCrop_noTransformation_appliesCenterCropTransformation() = runTest { val resourceId = android.R.drawable.star_big_on val expectedDrawable = loadExpectedDrawable(resourceId) { it.centerCrop() } glideComposeRule.setContent { ContentScaleGlideImage(model = resourceId, contentScale = ContentScale.Crop) } glideComposeRule.onNodeWithDefaultContentDescription().assertDisplays(expectedDrawable) } @Test fun glideImage_withContentScaleCrop_explicitTransformation_usesExplicitTransformation() = runTest { val resourceId = android.R.drawable.star_big_on val expectedDrawable = loadExpectedDrawable(resourceId) { it.centerInside() } glideComposeRule.setContent { ContentScaleGlideImage(model = resourceId, contentScale = ContentScale.Crop) { it.centerInside() } } glideComposeRule.onNodeWithDefaultContentDescription().assertDisplays(expectedDrawable) } private suspend fun RequestBuilder.loadRequiringSuccess() = (this.flow().first { it.status == Status.SUCCEEDED } as Resource).resource private suspend fun loadExpectedDrawable( @DrawableRes resourceId: Int, transformation: (RequestBuilder) -> RequestBuilder = { it -> it }, ): Drawable = transformation( Glide.with(context) .load(resourceId) .override(WIDTH.dpToPixels(), HEIGHT.dpToPixels()) ) .loadRequiringSuccess() @Composable private fun ContentScaleGlideImage( model: Any?, contentScale: ContentScale, requestBuilderTransform: RequestBuilderTransform = { it -> it }, ) = GlideImage( model = model, contentDescription = Constants.DEFAULT_DESCRIPTION, modifier = SIZE_MODIFIER, contentScale = contentScale, requestBuilderTransform = requestBuilderTransform, ) companion object { const val WIDTH = 25 // non-square const val HEIGHT = 30 val SIZE_MODIFIER = Modifier.size(WIDTH.dp, HEIGHT.dp) } } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageErrorTest.kt ================================================ @file:OptIn(ExperimentalGlideComposeApi::class) package com.bumptech.glide.integration.compose import android.content.Context import android.graphics.drawable.Drawable import androidx.compose.ui.test.assert import androidx.compose.ui.test.onNodeWithContentDescription import androidx.test.core.app.ApplicationProvider import com.bumptech.glide.integration.compose.test.GlideComposeRule import com.bumptech.glide.integration.compose.test.expectDisplayedDrawable import com.bumptech.glide.integration.compose.test.expectDisplayedResource import com.bumptech.glide.integration.compose.test.expectNoDrawable import org.junit.Rule import org.junit.Test /** * Avoids [com.bumptech.glide.load.engine.executor.GlideIdlingResourceInit] because we want to make * assertions about loads that have not yet completed. */ class GlideImageErrorTest { private val context: Context = ApplicationProvider.getApplicationContext() @get:Rule val glideComposeRule = GlideComposeRule() @Test fun requestBuilderTransform_withErrorResourceId_displaysError() { val description = "test" val errorResourceId = android.R.drawable.star_big_off glideComposeRule.setContent { GlideImage(model = null, contentDescription = description) { it.error(errorResourceId) } } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(errorResourceId)) } @Test fun requestBuilderTransform_withErrorDrawable_displaysError() { val description = "test" val errorDrawable = context.getDrawable(android.R.drawable.star_big_off) glideComposeRule.setContent { GlideImage(model = null, contentDescription = description) { it.error(errorDrawable) } } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(errorDrawable)) } @Test fun failureParameter_withErrorResourceId_displaysError() { val description = "test" val failureResourceId = android.R.drawable.star_big_off glideComposeRule.setContent { GlideImage( model = null, contentDescription = description, failure = placeholder(failureResourceId), ) } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(failureResourceId)) } @Test fun failureParameter_withDrawable_displaysDrawable() { val description = "test" val failureDrawable = context.getDrawable(android.R.drawable.star_big_off) glideComposeRule.setContent { GlideImage( model = null, contentDescription = description, failure = placeholder(failureDrawable), ) } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(failureDrawable)) } @Test fun failureParameter_withNullDrawable_displaysNothing() { val description = "test" glideComposeRule.setContent { GlideImage( model = null, contentDescription = description, failure = placeholder(null as Drawable?), ) } glideComposeRule.onNodeWithContentDescription(description).assert(expectNoDrawable()) } @Test fun failureParameter_withComposable_displaysComposable() { val failureResourceId = android.R.drawable.star_big_off val description = "test" glideComposeRule.setContent { GlideImage( model = null, contentDescription = "none", failure = placeholder { // Nesting GlideImage is not really a good idea, but it's convenient for // this test // because // we can use our helpers to assert on its contents. GlideImage( model = null, contentDescription = description, failure = placeholder(failureResourceId), ) }, ) } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(failureResourceId)) } @Test fun failure_setViaFailureParameterWithResourceId_andRequestBuilderTransform_prefersFailureParameter() { val description = "test" val failureResourceId = android.R.drawable.star_big_off glideComposeRule.setContent { GlideImage( model = null, contentDescription = description, failure = placeholder(failureResourceId), ) { it.error(android.R.drawable.btn_star) } } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(failureResourceId)) } @Test fun failure_setViaFailureParameterWithDrawable_andRequestBuilderTransform_prefersFailureParameter() { val description = "test" val failureDrawable = context.getDrawable(android.R.drawable.star_big_off) glideComposeRule.setContent { GlideImage( model = null, contentDescription = description, failure = placeholder(failureDrawable), ) { it.error(android.R.drawable.btn_star) } } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(failureDrawable)) } @Test fun failure_setViaFailureParameterWithNullDrawable_andRequestBuilderTransformWithNonNullDrawable_showsNoPlaceholder() { val description = "test" glideComposeRule.setContent { GlideImage( model = null, contentDescription = description, failure = placeholder(null as Drawable?), ) { it.error(android.R.drawable.btn_star) } } glideComposeRule.onNodeWithContentDescription(description).assert(expectNoDrawable()) } @Test fun failure_setViaFailureParameterWithComposable_andRequestBuilderTransform_showsComposable() { val description = "test" val failureResourceId = android.R.drawable.star_big_off glideComposeRule.setContent { GlideImage( model = null, contentDescription = "other", failure = placeholder { GlideImage( model = null, contentDescription = description, failure = placeholder(failureResourceId), ) }, ) { it.error(android.R.drawable.btn_star) } } glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(failureResourceId)) } } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImagePlaceholderTest.kt ================================================ @file:OptIn(ExperimentalGlideComposeApi::class) package com.bumptech.glide.integration.compose import android.content.Context import android.graphics.drawable.Drawable import androidx.compose.ui.test.assert import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.test.core.app.ApplicationProvider import com.bumptech.glide.integration.compose.test.expectDisplayedDrawable import com.bumptech.glide.integration.compose.test.expectDisplayedResource import com.bumptech.glide.integration.compose.test.expectNoDrawable import com.bumptech.glide.testutil.TearDownGlide import com.bumptech.glide.testutil.WaitModelLoaderRule import org.junit.Rule import org.junit.Test /** * Avoids [com.bumptech.glide.load.engine.executor.GlideIdlingResourceInit] and * [com.bumptech.glide.integration.compose.test.GlideComposeRule] because we want to make assertions * about loads that have not yet completed. */ class GlideImagePlaceholderTest { private val context: Context = ApplicationProvider.getApplicationContext() @get:Rule(order = 1) val composeRule = createComposeRule() @get:Rule(order = 2) val waitModelLoaderRule = WaitModelLoaderRule() @get:Rule(order = 3) val tearDownGlide = TearDownGlide() @Test fun requestBuilderTransform_withPlaceholderResourceId_displaysPlaceholder() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderResourceId = android.R.drawable.star_big_off composeRule.setContent { GlideImage(model = waitModel, contentDescription = description) { it.placeholder(placeholderResourceId) } } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(placeholderResourceId)) } @Test fun requestBuilderTransform_withPlaceholderDrawable_displaysPlaceholder() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderDrawable = context.getDrawable(android.R.drawable.star_big_off) composeRule.setContent { GlideImage(model = waitModel, contentDescription = description) { it.placeholder(placeholderDrawable) } } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(placeholderDrawable)) } @Test fun loadingParameter_withResourceId_displaysResource() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderResourceId = android.R.drawable.star_big_off composeRule.setContent { GlideImage( model = waitModel, contentDescription = description, loading = placeholder(placeholderResourceId), ) } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(placeholderResourceId)) } @Test fun loadingParameter_withDrawable_displaysResource() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderDrawable = context.getDrawable(android.R.drawable.star_big_off) composeRule.setContent { GlideImage( model = waitModel, contentDescription = description, loading = placeholder(placeholderDrawable), ) } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(placeholderDrawable)) } @Test fun loadingParameter_withNullDrawable_displaysNothing() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) composeRule.setContent { GlideImage( model = waitModel, contentDescription = description, loading = placeholder(null as Drawable?), ) } composeRule.onNodeWithContentDescription(description).assert(expectNoDrawable()) } @Test fun loadingParameter_withComposable_displaysComposable() { val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderResourceId = android.R.drawable.star_big_off val description = "test" composeRule.setContent { GlideImage( model = waitModel, contentDescription = "none", loading = placeholder { // Nesting GlideImage is not really a good idea, but it's convenient for // this test // because // we can use our helpers to assert on its contents. GlideImage( model = waitModel, contentDescription = description, loading = placeholder(placeholderResourceId), ) }, ) } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(placeholderResourceId)) } @Test fun loading_setViaLoadingParameterWithResourceId_andRequestBuilderTransform_prefersLoadingParameter() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderResourceId = android.R.drawable.star_big_off composeRule.setContent { GlideImage( model = waitModel, contentDescription = description, loading = placeholder(placeholderResourceId), ) { it.placeholder(android.R.drawable.btn_star) } } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(placeholderResourceId)) } @Test fun loading_setViaLoadingParameterWithDrawable_andRequestBuilderTransform_prefersLoadingParameter() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderDrawable = context.getDrawable(android.R.drawable.star_big_off) composeRule.setContent { GlideImage( model = waitModel, contentDescription = description, loading = placeholder(placeholderDrawable), ) { it.placeholder(android.R.drawable.btn_star) } } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(placeholderDrawable)) } @Test fun loading_setViaLoadingParameterWithNullDrawable_andRequestBuilderTransform_showsNoResource() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) composeRule.setContent { GlideImage( model = waitModel, contentDescription = description, loading = placeholder(null as Drawable?), ) { it.placeholder(android.R.drawable.btn_star) } } composeRule.onNodeWithContentDescription(description).assert(expectNoDrawable()) } @Test fun loading_setViaLoadingParameterWithComposable_andRequestBuilderTransform_showsComposable() { val description = "test" val waitModel = waitModelLoaderRule.waitOn(android.R.drawable.star_big_on) val placeholderResourceId = android.R.drawable.star_big_off composeRule.setContent { GlideImage( model = waitModel, contentDescription = "other", loading = placeholder { GlideImage( model = waitModel, contentDescription = description, loading = placeholder(placeholderResourceId), ) }, ) { it.placeholder(android.R.drawable.btn_star) } } composeRule .onNodeWithContentDescription(description) .assert(expectDisplayedResource(placeholderResourceId)) } } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/GlideImageTest.kt ================================================ @file:OptIn(ExperimentalGlideComposeApi::class, InternalGlideApi::class) package com.bumptech.glide.integration.compose import android.content.Context import android.graphics.drawable.Drawable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.test.assert import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.core.app.ApplicationProvider import com.bumptech.glide.Glide import com.bumptech.glide.integration.compose.test.GlideComposeRule import com.bumptech.glide.integration.compose.test.assertDisplays import com.bumptech.glide.integration.compose.test.bitmapSize import com.bumptech.glide.integration.compose.test.dpToPixels import com.bumptech.glide.integration.compose.test.expectDisplayedDrawable import com.bumptech.glide.integration.compose.test.expectDisplayedDrawableSize import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.integration.ktx.Size import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.google.common.truth.Truth.assertThat import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import org.junit.Rule import org.junit.Test class GlideImageTest { private val context: Context = ApplicationProvider.getApplicationContext() @get:Rule val glideComposeRule = GlideComposeRule() @Test fun glideImage_noModifierSize_resourceDrawable_displaysDrawable() { val description = "test" val resourceId = android.R.drawable.star_big_on glideComposeRule.setContent { GlideImage(model = resourceId, contentDescription = description) } glideComposeRule.waitForIdle() val expectedSize = resourceId.bitmapSize() glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawableSize(expectedSize)) } @Test fun glideImage_withSizeLargerThanImage_noTransformSet_doesNotUpscaleImage() { val description = "test" val resourceId = android.R.drawable.star_big_on glideComposeRule.setContent { GlideImage( model = resourceId, contentDescription = description, modifier = Modifier.size(300.dp, 300.dp), ) } glideComposeRule.waitForIdle() val expectedSize = resourceId.bitmapSize() glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawableSize(expectedSize)) } @Test fun glideImage_withChangingModel_refreshes() { val description = "test" val firstDrawable: Drawable = context.getDrawable(android.R.drawable.star_big_off)!! val secondDrawable: Drawable = context.getDrawable(android.R.drawable.star_big_on)!! glideComposeRule.setContent { val model = remember { mutableStateOf(firstDrawable) } fun swapModel() { model.value = secondDrawable } Column { TextButton(onClick = ::swapModel) { Text(text = "Swap") } GlideImage( model = model.value, modifier = Modifier.size(100.dp), contentDescription = description, ) } } glideComposeRule.waitForIdle() glideComposeRule.onNodeWithText("Swap").performClick() glideComposeRule.waitForIdle() glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(secondDrawable)) } @Test fun glideImage_withSizeLargerThanImage_upscaleTransformSet_upscalesImage() { val viewDimension = 300 val description = "test" val sizeRef = AtomicReference() glideComposeRule.setContent { GlideImage( model = android.R.drawable.star_big_on, requestBuilderTransform = { it.fitCenter() }, contentDescription = description, modifier = Modifier.size(viewDimension.dp, viewDimension.dp), ) with(LocalDensity.current) { val pixels = viewDimension.dp.roundToPx() sizeRef.set(Size(pixels, pixels)) } } glideComposeRule.waitForIdle() val pixels = sizeRef.get() glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawableSize(pixels)) } @Test fun glideImage_withThumbnail_prefersFullSizeImage() { val description = "test" val thumbnailDrawable = context.getDrawable(android.R.drawable.star_big_off) val fullsizeDrawable = context.getDrawable(android.R.drawable.star_big_on) glideComposeRule.setContent { GlideImage( model = fullsizeDrawable, requestBuilderTransform = { it.thumbnail(Glide.with(context).load(thumbnailDrawable)) }, contentDescription = description, ) } glideComposeRule.waitForIdle() glideComposeRule .onNodeWithContentDescription(description) .assert(expectDisplayedDrawable(fullsizeDrawable)) } @Test fun glideImage_withZeroSize_doesNotStartLoad() { val description = "test" glideComposeRule.setContent { Box(modifier = Modifier.size(0.dp)) { GlideImage(model = android.R.drawable.star_big_on, contentDescription = description) } } glideComposeRule.waitForIdle() glideComposeRule.onNodeWithContentDescription(description).assertDisplays(null) } @Test fun glideImage_withNegativeSize_doesNotStartLoad() { val description = "test" glideComposeRule.setContent { Box(modifier = Modifier.size((-10).dp)) { GlideImage(model = android.R.drawable.star_big_on, contentDescription = description) } } glideComposeRule.waitForIdle() glideComposeRule.onNodeWithContentDescription(description).assertDisplays(null) } @Test fun glideImage_withZeroWidth_validHeight_doesNotStartLoad() { val description = "test" glideComposeRule.setContent { Box(modifier = Modifier.size(0.dp, 10.dp)) { GlideImage(model = android.R.drawable.star_big_on, contentDescription = description) } } glideComposeRule.waitForIdle() glideComposeRule.onNodeWithContentDescription(description).assertDisplays(null) } @Test fun glideImage_withValidWidth_zeroHeight_doesNotStartLoad() { val description = "test" glideComposeRule.setContent { Box(modifier = Modifier.size(10.dp, 0.dp)) { GlideImage(model = android.R.drawable.star_big_on, contentDescription = description) } } glideComposeRule.waitForIdle() glideComposeRule.onNodeWithContentDescription(description).assertDisplays(null) } @Test fun glideImage_withZeroSize_thenValidSize_startsLoadWithValidSize() { val description = "test" val resourceId = android.R.drawable.star_big_on val validSizeDp = 10 glideComposeRule.setContent { val currentSize = remember { mutableStateOf(0.dp) } fun swapSize() { currentSize.value = validSizeDp.dp } TextButton(onClick = ::swapSize) { Text(text = "Swap") } Box(modifier = Modifier.size(currentSize.value)) { GlideImage(model = resourceId, contentDescription = description) } } glideComposeRule.waitForIdle() glideComposeRule.onNodeWithText("Swap").performClick() glideComposeRule.waitForIdle() glideComposeRule .onNodeWithContentDescription(description) .assert( expectDisplayedDrawableSize( Size(validSizeDp.dpToPixels(), validSizeDp.dpToPixels()) ) ) } @Test fun glideImage_withZeroSize_thenMultipleValidSizes_startsLoadWithFirstValidSize() { val description = "test" val resourceId = android.R.drawable.star_big_on val validSizeDps = listOf(10, 20, 30, 40) glideComposeRule.setContent { val currentSize = remember { mutableStateOf(0.dp) } val currentSizeIndex = remember { mutableStateOf(0) } fun swapSize() { currentSize.value = validSizeDps[currentSizeIndex.value].dp currentSizeIndex.value++ } TextButton(onClick = ::swapSize) { Text(text = "Swap") } Box(modifier = Modifier.size(currentSize.value)) { GlideImage(model = resourceId, contentDescription = description) } } repeat(validSizeDps.size) { glideComposeRule.waitForIdle() glideComposeRule.onNodeWithText("Swap").performClick() } glideComposeRule.waitForIdle() val expectedSize = validSizeDps[0] glideComposeRule .onNodeWithContentDescription(description) .assert( expectDisplayedDrawableSize( Size(expectedSize.dpToPixels(), expectedSize.dpToPixels()) ) ) } @Test fun glideImage_withSuccessfulResource_callsOnResourceReadyOnce() { val onResourceReadyCounter = AtomicInteger() val requestListener = object : RequestListener { override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean, ): Boolean { throw UnsupportedOperationException() } override fun onResourceReady( resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean, ): Boolean { onResourceReadyCounter.incrementAndGet() return false } } glideComposeRule.setContent { GlideImage(model = android.R.drawable.star_big_on, contentDescription = "") { it.listener(requestListener) } } glideComposeRule.waitForIdle() assertThat(onResourceReadyCounter.get()).isEqualTo(1) } } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/RememberGlidePreloadingDataTest.kt ================================================ @file:OptIn(ExperimentalGlideComposeApi::class, ExperimentalGlideComposeApi::class) package com.bumptech.glide.integration.compose import android.content.Context import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToIndex import androidx.compose.ui.unit.dp import androidx.test.core.app.ApplicationProvider import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.integration.compose.test.GlideComposeRule import com.bumptech.glide.request.target.Target import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test class RememberGlidePreloadingDataTest { private val context: Context = ApplicationProvider.getApplicationContext() @get:Rule val glideComposeRule = GlideComposeRule() @Test fun rememberGlidePreloadingData_withoutScroll_preloadsNextItem() { glideComposeRule.setContent { val preloadingData = rememberOneItemAtATimePreloadingData() LazyRow(modifier = Modifier.testTag(listTestTag)) { items(preloadingData.size) { index -> preloadingData.triggerPreload(index) GlideImage( model = model, contentDescription = imageContentDescription(index), Modifier.fillParentMaxWidth(), ) } } } assertThatModelIsInMemoryCache(preloadModels[1]) } @Test fun glideLazyListPreloader_onScroll_preloadsAheadInDirectionOfScroll() { glideComposeRule.setContent { val preloadingData = rememberOneItemAtATimePreloadingData() LazyRow(modifier = Modifier.testTag(listTestTag)) { items(preloadingData.size) { index -> preloadingData.triggerPreload(index) GlideImage( model = model, contentDescription = imageContentDescription(index), Modifier.fillParentMaxWidth(), ) } } } val scrollToIndex = 1 glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) assertThatModelIsInMemoryCache(preloadModels[2]) } @Test fun glideLazyListPreloader_withHeaderItem_onScroll_doesNotCrash() { glideComposeRule.setContent { val preloadingData = rememberOneItemAtATimePreloadingData() LazyRow(modifier = Modifier.testTag(listTestTag)) { item { Text(text = "Header") } items(preloadingData.size) { index -> preloadingData.triggerPreload(index) GlideImage( model = model, contentDescription = imageContentDescription(index), Modifier.fillParentMaxWidth(), ) } } } // Scroll to the 0th image, accounting for the first header item. val scrollToIndex = 1 glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) // Make sure the next image, the 1th, is in memory due to preloading. assertThatModelIsInMemoryCache(preloadModels[1]) } @Test fun glideLazyListPreloader_whenDataChanges_onScroll_preloadsUpdatedData() { glideComposeRule.setContent { // Swap both to avoid confusing the preloader. The preloader doesn't notice or take into // account data set changes (this is a bug in the Java preloading API)... val currentPreloadModels = remember { mutableStateListOf() } val currentModels = remember { mutableStateListOf() } // Use a button to swap data because we can't mutate state in setContent easily from // outside // the method, nor can you call setContent multiple times. fun swapData() { currentPreloadModels.addAll(preloadModels) currentModels.addAll(preloadModels) } val preloadData = rememberGlidePreloadingData( data = currentPreloadModels, preloadImageSize = Target.SIZE_ORIGINAL.toSize(), numberOfItemsToPreload = 1, fixedVisibleItemCount = 1, ) { data: Int, requestBuilder: RequestBuilder -> requestBuilder.load(data).removeTheme() } TextButton(onClick = ::swapData) { Text(text = "Swap") } Column { LazyRow( modifier = Modifier.testTag(listTestTag), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { items(currentModels.size) { index -> // This mismatch between currentModels and preloadData may lead to errors in // the future // because items may be recomposed before the setContent method's function // is // recomposed. See https://chat.google.com/room/AAAAYRnp4-Y/AvFrBgb_peU for // a bunch of // detailed discussion. preloadData.triggerPreload(index) GlideImage( model = currentModels[index], contentDescription = imageContentDescription(index), Modifier.fillParentMaxWidth(), ) } } } } glideComposeRule.onNodeWithText("Swap").performClick() glideComposeRule.waitForIdle() val scrollToIndex = 1 glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) assertThatModelIsInMemoryCache(preloadModels[scrollToIndex + 1]) } @Test fun glideLazyListPreloader_withHeaderItems_andPositionFunction_onScroll_preloadsTheFirstItem() { val numHeaderItems = 3 glideComposeRule.setContent { val data = rememberOneItemAtATimePreloadingData() LazyRow(modifier = Modifier.testTag(listTestTag)) { repeat(numHeaderItems) { item { Text(text = "Header$it") } } items(data.size) { index -> data.triggerPreload(index) GlideImage( model = model, contentDescription = imageContentDescription(index), Modifier.fillParentMaxWidth(), ) } } } val imageIndex = 1 val scrollToIndex = numHeaderItems + imageIndex glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) assertThatModelIsInMemoryCache(preloadModels[imageIndex + 1]) } // Ignore the preload request because we want to test that the preloader loaded a model // and not be confused by our UI loading a model. Do not ignore the preload request // builder in real code! @Composable private fun GlidePreloadingData.triggerPreload(index: Int) = this[index].first @Composable private fun rememberOneItemAtATimePreloadingData(): GlidePreloadingData { return rememberGlidePreloadingData( data = preloadModels, preloadImageSize = Target.SIZE_ORIGINAL.toSize(), numberOfItemsToPreload = 1, fixedVisibleItemCount = 1, ) { model, requestBuilder -> requestBuilder.load(model).removeTheme() } } private fun assertThatModelIsInMemoryCache(@DrawableRes model: Int) { // Wait for previous async image loads to finish glideComposeRule.waitForIdle() val nextPreloadModel: Drawable = Glide.with(context).load(model).removeTheme().onlyRetrieveFromCache(true).submit().get() assertThat(nextPreloadModel).isNotNull() } // We're loading the same resource across two different Contexts. One is the Context from the // instrumentation package, the other is the package under test. Each Context has it's own // Theme, // neither of which are equal to each other. So that we can verify an item is loaded into // memory, // we remove the themes from all requests that we need to have matching cache keys. private fun RequestBuilder.removeTheme() = theme(null) private companion object { const val model = android.R.drawable.star_big_on // Use different preload and non-preload models so that we can assert on which items are // preloaded and not loaded by the list. This is bad practice in production code and would // waste // resources while doing nothing useful in a real app. val preloadModels = listOf( android.R.drawable.btn_minus, android.R.drawable.btn_radio, android.R.drawable.btn_star, ) const val listTestTag = "listTestTag" fun imageContentDescription(index: Int) = "Image $index" } } private fun Int.toSize() = this.toFloat().let { Size(it, it) } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/test/GlideComposeRule.kt ================================================ package com.bumptech.glide.integration.compose.test import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import com.bumptech.glide.load.engine.executor.GlideIdlingResourceInit import com.bumptech.glide.testutil.TearDownGlide import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement /** * Merges [TearDownGlide], [ComposeContentTestRule] and [GlideIdlingResourceInit] into a single * helper rule that's common across (most of) Glide's compose integration tests. */ class GlideComposeRule(private val composeRule: ComposeContentTestRule = createComposeRule()) : TestRule, ComposeContentTestRule by composeRule { private val rules = RuleChain.outerRule(TearDownGlide()).around(composeRule) override fun apply(base: Statement?, description: Description?): Statement { return rules.apply( object : Statement() { override fun evaluate() { GlideIdlingResourceInit.initGlide(this@GlideComposeRule) base?.evaluate() } }, description, ) } } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/test/expectations.kt ================================================ @file:OptIn(InternalGlideApi::class) package com.bumptech.glide.integration.compose.test import android.content.Context import android.content.res.Resources import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.TypedValue import androidx.compose.runtime.MutableState import androidx.compose.ui.semantics.SemanticsPropertyKey import androidx.compose.ui.test.SemanticsMatcher import androidx.test.core.app.ApplicationProvider import com.bumptech.glide.integration.compose.DisplayedDrawableKey import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.integration.ktx.Size import kotlin.math.roundToInt private fun context(): Context = ApplicationProvider.getApplicationContext() fun Int.dpToPixels() = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics, ) .roundToInt() fun Int.bitmapSize() = context().resources.getDrawable(this, context().theme).size() fun Drawable.size() = (this as BitmapDrawable).bitmap.let { Size(it.width, it.height) } fun expectDisplayedResource(resourceId: Int) = expectDisplayedDrawable(context().getDrawable(resourceId)) fun Drawable?.bitmapOrThrow(): Bitmap? = if (this == null) null else (this as BitmapDrawable).bitmap fun expectDisplayedDrawableSize(expectedSize: Size): SemanticsMatcher = expectDisplayedDrawable(expectedSize) { it?.size() } fun expectDisplayedDrawable(expectedValue: Drawable?): SemanticsMatcher = expectDisplayedDrawable(expectedValue.bitmapOrThrow(), ::compareBitmaps) { it.bitmapOrThrow() } fun expectNoDrawable(): SemanticsMatcher = expectDisplayedDrawable(null) private fun compareBitmaps(first: Bitmap?, second: Bitmap?): Boolean { if (first == null && second == null) { return true } if (first == null || second == null) { return false } return first.sameAs(second) } private fun expectDisplayedDrawable( expectedValue: ValueT, compare: (ValueT?, ValueT?) -> Boolean = { first, second -> first == second }, transform: (Drawable?) -> ValueT, ): SemanticsMatcher = expectStateValue(DisplayedDrawableKey, expectedValue, compare) { transform(it) } private fun expectStateValue( key: SemanticsPropertyKey>, expectedValue: TransformedValueT, compare: (TransformedValueT?, TransformedValueT?) -> Boolean, transform: (ValueT?) -> TransformedValueT?, ): SemanticsMatcher = SemanticsMatcher("${key.name} = '$expectedValue'") { val value = transform(it.config.getOrElseNullable(key) { null }?.value) if (!compare(value, expectedValue)) { throw AssertionError("Expected: $expectedValue, but was: $value") } true } fun expectSameInstance(expectedDrawable: Drawable) = SemanticsMatcher("${DisplayedDrawableKey.name} = '$expectedDrawable'") { val actualValue: Drawable? = it.config.getOrElseNullable(DisplayedDrawableKey) { null }?.value if (actualValue !== expectedDrawable) { throw AssertionError("Expected: $expectedDrawable, but was: $actualValue") } true } ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/test/nodes.kt ================================================ package com.bumptech.glide.integration.compose.test import android.app.Application import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assert import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.test.core.app.ApplicationProvider object Constants { const val DEFAULT_DESCRIPTION = "test" } fun ComposeContentTestRule.onNodeWithDefaultContentDescription() = onNodeWithContentDescription(Constants.DEFAULT_DESCRIPTION) fun SemanticsNodeInteraction.assertDisplays(@DrawableRes resourceId: Int) = assertDisplays(ApplicationProvider.getApplicationContext().getDrawable(resourceId)) fun SemanticsNodeInteraction.assertDisplays(drawable: Drawable?) = assert(expectDisplayedDrawable(drawable)) fun SemanticsNodeInteraction.assertDisplaysInstance(drawable: Drawable) = assert(expectSameInstance(drawable)) ================================================ FILE: integration/compose/src/androidTest/java/com/bumptech/glide/load/engine/executor/GlideIdlingResourceInit.kt ================================================ package com.bumptech.glide.load.engine.executor import androidx.compose.ui.test.IdlingResource import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.idling.concurrent.IdlingThreadPoolExecutor import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit object GlideIdlingResourceInit { fun initGlide(composeRule: ComposeTestRule) { val executor = IdlingThreadPoolExecutor( "glide_test_thread", /* corePoolSize = */ 1, /* maximumPoolSize = */ 1, /* keepAliveTime = */ 1, TimeUnit.SECONDS, LinkedBlockingQueue(), ) { Thread(it) } composeRule.registerIdlingResource( object : IdlingResource { override val isIdleNow: Boolean get() = executor.isIdleNow } ) val glideExecutor = GlideExecutor(executor) Glide.init( ApplicationProvider.getApplicationContext(), GlideBuilder() .setSourceExecutor(glideExecutor) .setAnimationExecutor(glideExecutor) .setDiskCacheExecutor(glideExecutor), ) } } ================================================ FILE: integration/compose/src/main/java/com/bumptech/glide/integration/compose/ExperimentalGlideComposeApi.kt ================================================ package com.bumptech.glide.integration.compose @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "Glide's Compose integration is experimental. APIs may change or be removed without" + " warning.", ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) public annotation class ExperimentalGlideComposeApi ================================================ FILE: integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideImage.kt ================================================ package com.bumptech.glide.integration.compose import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.SemanticsPropertyKey import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.semantics import androidx.lifecycle.compose.LocalLifecycleOwner import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestManager import com.bumptech.glide.integration.ktx.AsyncGlideSize import com.bumptech.glide.integration.ktx.ExperimentGlideFlows import com.bumptech.glide.integration.ktx.ImmediateGlideSize import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.integration.ktx.ResolvableGlideSize import com.bumptech.glide.integration.ktx.Size import com.bumptech.glide.integration.ktx.Status import com.google.accompanist.drawablepainter.rememberDrawablePainter /** Mutates and returns the given [RequestBuilder] to apply relevant options. */ public typealias RequestBuilderTransform = (RequestBuilder) -> RequestBuilder /** * Start a request by passing [model] to [RequestBuilder.load] using the given [requestManager] and * then applying the [requestBuilderTransform] function to add options or apply mutations if the * caller desires. * * [alignment], [contentScale], [alpha], [colorFilter] and [contentDescription] have the same * defaults (if any) and function identically to the parameters in [Image]. * * If you want to restrict the size of this [Composable], use the given [modifier]. If you'd like to * force the size of the pixels you load to be different than the display area, use * [RequestBuilder.override]. Often you can get better performance by setting an explicit size so * that we do not have to wait for layout to fetch the image. If the size set via the [modifier] is * dependent on the content, Glide will probably end up loading the image using * [com.bumptech.glide.request.target.Target.SIZE_ORIGINAL]. Avoid `SIZE_ORIGINAL`, implicitly or * explicitly if you can. You may end up loading a substantially larger image than you need, which * will increase memory usage and may also increase latency. * * If you provide your own [requestManager] rather than using this method's default, consider using * [remember] at a higher level to avoid some amount of overhead of retrieving it each * re-composition. * * This method will inspect [contentScale] and apply a matching transformation if one exists. Any * automatically applied transformation can be overridden using [requestBuilderTransform]. Either * apply a specific transformation instead, or use [RequestBuilder.dontTransform]] * * Transitions set via [RequestBuilder.transition] are currently ignored. * * Note - this method is likely to change while we work on improving the API. Transitions are one * significant unexplored area. It's also possible we'll try and remove the [RequestBuilder] from * the direct API and instead allow all options to be set directly in the method. * * [requestBuilderTransform] is overridden by any overlapping parameter defined in this method if * that parameter is non-null. For example, [loading] and [failure], if non-null will be used in * place of any placeholder set by [requestBuilderTransform] using [RequestBuilder.placeholder] or * [RequestBuilder.error]. * * @param loading A [Placeholder] that will be displayed while the request is loading. Specifically * it's used if the request is cleared ([com.bumptech.glide.request.target.Target.onLoadCleared]) * or loading ([com.bumptech.glide.request.target.Target.onLoadStarted]. There's a subtle * difference in behavior depending on which type of [Placeholder] you use. The resource and * `Drawable` variants will be displayed if the request fails and no other failure handling is * specified, but the `Composable` will not. * @param failure A [Placeholder] that will be displayed if the request fails. Specifically it's * used when [com.bumptech.glide.request.target.Target.onLoadFailed] is called. If * [RequestBuilder.error] is called in [requestBuilderTransform] with a valid [RequestBuilder] (as * opposed to resource id or [Drawable]), this [Placeholder] will not be used unless the `error` * [RequestBuilder] also fails. This parameter does not override error [RequestBuilder]s, only * error resource ids and/or [Drawable]s. */ // TODO(judds): the API here is not particularly composeesque, we should consider alternatives // to RequestBuilder (though thumbnail() may make that a challenge). // TODO(judds): Consider how to deal with transitions. @ExperimentalGlideComposeApi @OptIn(InternalGlideApi::class) @Composable public fun GlideImage( model: Any?, contentDescription: String?, modifier: Modifier = Modifier, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null, // TODO(judds): Consider using separate GlideImage* methods instead of sealed classes. // See http://shortn/_x79pjkMZIH for an internal discussion. loading: Placeholder? = null, failure: Placeholder? = null, // TODO(judds): Consider defaulting to load the model here instead of always doing so below. requestBuilderTransform: RequestBuilderTransform = { it }, ) { val requestManager: RequestManager = LocalContext.current.let { remember(it) { Glide.with(it) } } val requestBuilder = rememberRequestBuilderWithDefaults( model, requestManager, requestBuilderTransform, contentScale, ) .let { loading?.apply(it::placeholder, it::placeholder) ?: it } .let { failure?.apply(it::error, it::error) ?: it } val overrideSize: Size? = requestBuilder.overrideSize() val (size, finalModifier) = rememberSizeAndModifier(overrideSize, modifier) // TODO(judds): It seems like we should be able to use the production paths for // resource / drawables as well as Composables. It's not totally clear what part of the prod // code // isn't supported. if (LocalInspectionMode.current && loading?.isResourceOrDrawable() == true) { PreviewResourceOrDrawable(loading, contentDescription, modifier) return } SizedGlideImage( requestBuilder = requestBuilder, size = size, modifier = finalModifier, contentDescription = contentDescription, alignment = alignment, contentScale = contentScale, alpha = alpha, colorFilter = colorFilter, placeholder = loading?.maybeComposable(), failure = failure?.maybeComposable(), ) } @OptIn(ExperimentalGlideComposeApi::class) @Composable private fun PreviewResourceOrDrawable( loading: Placeholder, contentDescription: String?, modifier: Modifier, ) { val drawable = when (loading) { is Placeholder.OfDrawable -> loading.drawable is Placeholder.OfResourceId -> LocalContext.current.getDrawable(loading.resourceId) is Placeholder.OfComposable -> throw IllegalArgumentException( "Composables should go through the production codepath" ) } Image( painter = rememberDrawablePainter(drawable), modifier = modifier, contentDescription = contentDescription, ) } /** * Used to specify a [Drawable] to use in conjunction with [GlideImage]'s `loading` or `failure` * parameters. * * Ideally [drawable] is non-null, but because [android.content.Context.getDrawable] can return * null, we allow it here. `placeholder(null)` has the same override behavior as if a non-null * `Drawable` were provided. */ @ExperimentalGlideComposeApi public fun placeholder(drawable: Drawable?): Placeholder = Placeholder.OfDrawable(drawable) /** * Used to specify a resource id to use in conjunction with [GlideImage]'s `loading` or `failure` * parameters. * * In addition to being slightly simpler than manually fetching a [Drawable] and passing it to * [placeholder], this method can be more efficient because the [Drawable] will only be loaded when * needed. */ @ExperimentalGlideComposeApi public fun placeholder(@DrawableRes resourceId: Int): Placeholder = Placeholder.OfResourceId(resourceId) /** * Used to specify a [Composable] function to use in conjunction with [GlideImage]'s `loading` or * `failure` parameter. * * Providing a nested [GlideImage] is not recommended. Use [RequestBuilder.thumbnail] or * [RequestBuilder.error] as an alternative. */ @ExperimentalGlideComposeApi public fun placeholder(composable: @Composable () -> Unit): Placeholder = Placeholder.OfComposable(composable) /** * Content to display during a particular state of a Glide Request, for example while the request is * loading or if the request fails. * * `of(Drawable)` and `of(resourceId)` trigger fewer recompositions than `of(@Composable () -> * Unit)` so you should only use the Composable variant if you require something more complex than a * simple color or a static image. * * `of(@Composable () -> Unit)` will display the [Composable] inside a [Box] whose modifier is the * one provided to [GlideImage]. Doing so allows Glide to infer the requested size if one is not * explicitly specified on the request itself. */ @ExperimentalGlideComposeApi public sealed class Placeholder { internal class OfDrawable(internal val drawable: Drawable?) : Placeholder() internal class OfResourceId(@DrawableRes internal val resourceId: Int) : Placeholder() internal class OfComposable(internal val composable: @Composable () -> Unit) : Placeholder() internal fun isResourceOrDrawable() = when (this) { is OfDrawable -> true is OfResourceId -> true is OfComposable -> false } internal fun maybeComposable(): (@Composable () -> Unit)? = when (this) { is OfComposable -> this.composable else -> null } internal fun apply( resource: (Int) -> RequestBuilder, drawable: (Drawable?) -> RequestBuilder, ): RequestBuilder = when (this) { is OfDrawable -> drawable(this.drawable) is OfResourceId -> resource(this.resourceId) // Clear out any previously set placeholder. else -> drawable(null) } } @OptIn(InternalGlideApi::class) private data class SizeAndModifier(val size: ResolvableGlideSize, val modifier: Modifier) @OptIn(InternalGlideApi::class) @Composable private fun rememberSizeAndModifier(overrideSize: Size?, modifier: Modifier) = remember(overrideSize, modifier) { if (overrideSize != null) { SizeAndModifier(ImmediateGlideSize(overrideSize), modifier) } else { val sizeObserver = SizeObserver() SizeAndModifier( AsyncGlideSize(sizeObserver::getSize), modifier.sizeObservingModifier(sizeObserver), ) } } @Composable private fun rememberRequestBuilderWithDefaults( model: Any?, requestManager: RequestManager, requestBuilderTransform: RequestBuilderTransform, contentScale: ContentScale, ) = remember(model, requestManager, requestBuilderTransform, contentScale) { requestBuilderTransform(requestManager.load(model).contentScaleTransform(contentScale)) } private fun RequestBuilder.contentScaleTransform( contentScale: ContentScale ): RequestBuilder { return when (contentScale) { ContentScale.Crop -> { optionalCenterCrop() } ContentScale.Inside, ContentScale.Fit -> { // Outside compose, glide would use fitCenter() for FIT. But that's probably not a good // decision given how unimportant Bitmap re-use is relative to minimizing texture sizes // now. // So instead we'll do something different and prefer not to upscale, which means using // centerInside(). The UI can still scale the view even if the Bitmap is smaller. optionalCenterInside() } else -> { this } } // TODO(judds): Think about how to handle the various fills } @OptIn(InternalGlideApi::class, ExperimentGlideFlows::class) @Composable private fun SizedGlideImage( requestBuilder: RequestBuilder, size: ResolvableGlideSize, modifier: Modifier, contentDescription: String?, alignment: Alignment, contentScale: ContentScale, alpha: Float, colorFilter: ColorFilter?, placeholder: @Composable (() -> Unit)?, failure: @Composable (() -> Unit)?, ) { // Use a Box so we can infer the size if the request doesn't have an explicit size. @Composable fun @Composable () -> Unit.boxed() = Box(modifier = modifier) { this@boxed() } val painter = rememberGlidePainter(requestBuilder = requestBuilder, size = size) if (placeholder != null && painter.status.showPlaceholder()) { placeholder.boxed() } else if (failure != null && painter.status == Status.FAILED) { failure.boxed() } else { Image( painter = painter, contentDescription = contentDescription, alignment = alignment, contentScale = contentScale, alpha = alpha, colorFilter = colorFilter, modifier = modifier.then(Modifier.semantics { displayedDrawable = painter.currentDrawable }), ) } } @OptIn(ExperimentGlideFlows::class) private fun Status.showPlaceholder(): Boolean = when (this) { Status.RUNNING -> true Status.CLEARED -> true else -> false } @OptIn(InternalGlideApi::class) @Composable private fun rememberGlidePainter( requestBuilder: RequestBuilder, size: ResolvableGlideSize, ): GlidePainter { val scope = rememberCoroutineScope() val lifecycleOwner = LocalLifecycleOwner.current // TODO(judds): Calling onRemembered here manually might make a minor improvement in how quickly // the image load is started, but it also triggers a recomposition. I can't figure out why it // triggers a recomposition return remember(requestBuilder, size, lifecycleOwner) { GlidePainter(requestBuilder, size, scope, lifecycleOwner) } } @OptIn(InternalGlideApi::class) private fun Modifier.sizeObservingModifier(sizeObserver: SizeObserver): Modifier = this.layout { measurable, constraints -> val inferredSize = constraints.inferredGlideSize() if (inferredSize != null) { sizeObserver.setSize(inferredSize) } val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.place(0, 0) } } internal val DisplayedDrawableKey = SemanticsPropertyKey>("DisplayedDrawable") internal var SemanticsPropertyReceiver.displayedDrawable by DisplayedDrawableKey ================================================ FILE: integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlidePainter.kt ================================================ package com.bumptech.glide.integration.compose import android.graphics.drawable.Animatable import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import androidx.compose.runtime.MutableState import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import com.bumptech.glide.RequestBuilder import com.bumptech.glide.integration.ktx.ExperimentGlideFlows import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.integration.ktx.Placeholder import com.bumptech.glide.integration.ktx.ResolvableGlideSize import com.bumptech.glide.integration.ktx.Resource import com.bumptech.glide.integration.ktx.Status import com.bumptech.glide.integration.ktx.flowResolvable import com.google.accompanist.drawablepainter.DrawablePainter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.plus // This class is inspired by a similar implementation in the excellent Coil library // (https://github.com/coil-kt/coil), specifically: // https://github.com/coil-kt/coil/blob/main/coil-compose-base/src/main/java/coil/compose/AsyncImagePainter.kt @Stable internal class GlidePainter @OptIn(InternalGlideApi::class) constructor( private val requestBuilder: RequestBuilder, private val size: ResolvableGlideSize, scope: CoroutineScope, private val lifecycleOwner: LifecycleOwner, ) : Painter(), RememberObserver { @OptIn(ExperimentGlideFlows::class) internal var status: Status by mutableStateOf(Status.CLEARED) internal val currentDrawable: MutableState = mutableStateOf(null) private var alpha: Float by mutableStateOf(DefaultAlpha) private var colorFilter: ColorFilter? by mutableStateOf(null) private var delegate: Painter? by mutableStateOf(null) private val scope = scope + SupervisorJob(parent = scope.coroutineContext.job) + Dispatchers.Main.immediate private var currentJob: Job? = null init { scope.launch { // If the Lifecycle state is at least STARTED, start the animation. Otherwise, stop the // animation. lifecycleOwner.lifecycle.currentStateFlow.collect { if (it.isAtLeast(Lifecycle.State.STARTED)) { currentDrawable.value?.let { drawable -> if (drawable is Animatable) { drawable.start() } } } else { currentDrawable.value?.let { drawable -> if (drawable is Animatable) { drawable.stop() } } } } } } override val intrinsicSize: Size get() = delegate?.intrinsicSize ?: Size.Unspecified override fun DrawScope.onDraw() { delegate?.apply { draw(size, alpha, colorFilter) } } override fun onAbandoned() { (delegate as? RememberObserver)?.onAbandoned() } override fun onForgotten() { (delegate as? RememberObserver)?.onForgotten() currentJob?.cancel() currentJob = null currentDrawable.value = null delegate = null } override fun onRemembered() { (delegate as? RememberObserver)?.onRemembered() if (currentJob == null) { currentJob = launchRequest() } // In case the onRemembered is called after the lifecycle onStop, it will start the // animation, // stop it here again. if (!lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { currentDrawable.value?.let { drawable -> if (drawable is Animatable) { drawable.stop() } } } } @OptIn(ExperimentGlideFlows::class, InternalGlideApi::class) private fun launchRequest() = this.scope.launch { requestBuilder.flowResolvable(size).collect { updateDelegate( when (it) { is Resource -> it.resource is Placeholder -> it.placeholder } ) status = it.status } } private fun Drawable.toPainter() = when (this) { is BitmapDrawable -> BitmapPainter(bitmap.asImageBitmap()) is ColorDrawable -> ColorPainter(Color(color)) else -> DrawablePainter(mutate()) } private fun updateDelegate(drawable: Drawable?) { val newDelegate = drawable?.toPainter() val oldDelegate = delegate if (newDelegate !== oldDelegate) { (oldDelegate as? RememberObserver)?.onForgotten() (newDelegate as? RememberObserver)?.onRemembered() currentDrawable.value = drawable delegate = newDelegate } } override fun applyAlpha(alpha: Float): Boolean { this.alpha = alpha return true } override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { this.colorFilter = colorFilter return true } } ================================================ FILE: integration/compose/src/main/java/com/bumptech/glide/integration/compose/Preload.kt ================================================ package com.bumptech.glide.integration.compose import android.graphics.drawable.Drawable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.geometry.Size import androidx.compose.ui.platform.LocalContext import com.bumptech.glide.Glide import com.bumptech.glide.ListPreloader import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestManager private const val DEFAULT_ITEMS_TO_PRELOAD = 10 /** * Preloads ahead of the data access position on the returned [GlidePreloadingData], similar to * [ListPreloader] and [com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader]. * * The only time this API is useful is when your UI also loads an item with exactly the same * options, model and size. You can ensure you're doing so by using the [RequestBuilder] returned by * [GlidePreloadingData.get] * * Typical usage will look something like this: * ``` * val glidePreloadingData = * rememberGlidePreloadingData(myDataList, THUMBNAIL_SIZE) { myDataItem, requestBuilder -> * // THUMBNAIL_SIZE is applied for you, but .load() is not because determining the model from * // the underlying data isn't trivial. Don't forget to call .load()! * requestBuilder.load(myDataItem.url) * } * * LazyRow(...) { * item { Text(text = "Header") } * items(glidePreloadingData.size) { index -> * val (myDataItem, preloadRequest) = glidePreloadingData[index] * GlideImage(model = item.url, contentDescription = item.description, ...) { primaryRequest -> * primaryRequest.thumbnail(preloadRequest) * } * } * } * ``` * * Note that preloading will not occur until the first access of `glidePreloadingData`. If you have * multiple disjoint data sets that you'd like to preload, or have some number of preceding header * rows prior to your first image, you can optionally add a few manual calls to make preloading * continue smoothly across data sets. One way you might do so is to call the next data set toward * the end of the previous data set, e.g.: * * ``` * val itemsToPreload = 15 * items(firstDataSet.size) { index -> * ... // Do something with first data set. * * // Then as you get to the end of the first data set, start preloading the next data set * manually * if (index >= firstDataSet.size - itemsToPreload) { * nextDataSet[itemsToPreload - (firstDataSet.size - index)] * } * } * ``` * * @param dataSize The total number of items to display and preload. * @param dataGetter A getter for the item at the given index (ie [List.get]. * @param preloadImageSize The override size we'll pass to [RequestBuilder.override] . * @param numberOfItemsToPreload The number of items to preload ahead of the user's current * position. This should be tested for each application. If the total memory size of the preloaded * images exceeds the memory cache size, preloading for a lazy list is not effective. However if * you preload too few things, the buffer may be small enough that images are not available when * they could be, so it's always a balancing act. The smaller the preloaded image, the more you * can preload. * @param fixedVisibleItemCount The number of visible items. In some cases this can vary widely in * which case you can leave this value `null`. If the number of visible items is always one or * two, it might make sense to just set this to the larger of the two to reduce churn in the * preloader. * @param requestBuilderTransform See [ListPreloader.PreloadModelProvider.getPreloadRequestBuilder]. * You should call [RequestBuilder.load] on the given `item` so that any type specific options * applied the matching [RequestManager.load] method are applied identically to the preload * request. Remember that the request produced by this transform must exactly match the request * made in your non-preload request for preloading to be useful. */ @Composable public fun rememberGlidePreloadingData( dataSize: Int, dataGetter: (Int) -> DataT, preloadImageSize: Size, numberOfItemsToPreload: Int = DEFAULT_ITEMS_TO_PRELOAD, fixedVisibleItemCount: Int? = null, requestBuilderTransform: PreloadRequestBuilderTransform, ): GlidePreloadingData { val requestManager = LocalContext.current.let { remember(it) { Glide.with(it) } } return remember( requestManager, dataSize, dataGetter, preloadImageSize, numberOfItemsToPreload, fixedVisibleItemCount, requestBuilderTransform, ) { val preloaderData = PreloaderData(dataSize, dataGetter, requestBuilderTransform, preloadImageSize) val preloader = ListPreloader( requestManager, PreloadModelProvider(requestManager, preloaderData), PreloadDimensionsProvider(preloaderData), numberOfItemsToPreload, ) PreloadDataImpl( dataSize, dataGetter, requestManager, preloadImageSize, fixedVisibleItemCount, preloader, requestBuilderTransform, ) } } /** * A helper for [rememberGlidePreloadingData] that accepts a [List]. See the more general equivalent * for details. */ @Composable public fun rememberGlidePreloadingData( data: List, preloadImageSize: Size, numberOfItemsToPreload: Int = DEFAULT_ITEMS_TO_PRELOAD, fixedVisibleItemCount: Int? = null, requestBuilderTransform: PreloadRequestBuilderTransform, ): GlidePreloadingData { return rememberGlidePreloadingData( dataSize = data.size, dataGetter = data::get, preloadImageSize = preloadImageSize, numberOfItemsToPreload = numberOfItemsToPreload, fixedVisibleItemCount = fixedVisibleItemCount, requestBuilderTransform = requestBuilderTransform, ) } private data class PreloaderData( val dataSize: Int, val dataAccessor: (Int) -> DataT, val requestBuilderTransform: PreloadRequestBuilderTransform, val size: Size, ) { fun preloadRequests(requestManager: RequestManager, item: DataT): RequestBuilder { return requestBuilderTransform(item, requestManager.asDrawable()) } } /** * Wraps a set of data, triggers image preloads based on the positions provided to [get] and exposes * the data and the preload [RequestBuilder]. */ public interface GlidePreloadingData { /** The total number of items in the data set. */ public val size: Int /** * Returns the [DataT] at a given index in the data and a [RequestBuilder] that will trigger a * request that exactly matches the preload request for this index. * * The returned [RequestBuilder] should always be used to display the item at the given index. * Otherwise the preload request triggered by this call is likely useless work. The * [RequestBuilder] can either be used as the primary request, or more likely, passed as the * [RequestBuilder.thumbnail] to a higher resolution request. * * This method has side affects! Calling it will trigger preloads based on the given [index]. * Preloading assumes sequential access in a manner that matches what the user will see. If you * need to look up data at indices for other reasons, use the underlying data source directly so * that you do not confuse the preloader. Only use this method when obtaining data to display to * the user. */ @Composable public operator fun get(index: Int): Pair> } private class PreloadDataImpl( override val size: Int, private val indexToData: (Int) -> DataT, private val requestManager: RequestManager, private val preloadImageSize: Size, private val fixedVisibleItemCount: Int?, private val preloader: ListPreloader, private val requestBuilderTransform: PreloadRequestBuilderTransform, ) : GlidePreloadingData { @Composable override fun get(index: Int): Pair> { val item = indexToData(index) val requestBuilder = requestBuilderTransform( item, requestManager .asDrawable() .override(preloadImageSize.width.toInt(), preloadImageSize.height.toInt()), ) LaunchedEffect(preloader, preloadImageSize, requestBuilderTransform, indexToData, index) { preloader.onScroll(/* absListView= */ null, index, fixedVisibleItemCount ?: 1, size) } return item to requestBuilder } } private class PreloadDimensionsProvider( private val updatedData: PreloaderData ) : ListPreloader.PreloadSizeProvider { override fun getPreloadSize(item: DataT, adapterPosition: Int, perItemPosition: Int): IntArray = updatedData.size.toIntArray() } private fun Size.toIntArray() = intArrayOf(width.toInt(), height.toInt()) private class PreloadModelProvider( private val requestManager: RequestManager, private val data: PreloaderData, ) : ListPreloader.PreloadModelProvider { override fun getPreloadItems(position: Int): MutableList { return mutableListOf(data.dataAccessor(position)) } override fun getPreloadRequestBuilder(item: DataT): RequestBuilder<*> { return data.preloadRequests(requestManager, item) } } /** * Provides the data to load and a [RequestBuilder] to load it with. * * You must at least call [RequestBuilder.load] with the appropriate model extracted from `item` on * the given `requestBuilder`. You can also optionally call any other methods available on * `requestBuilder` to customize your load. */ public typealias PreloadRequestBuilderTransform = (item: DataTypeT, requestBuilder: RequestBuilder) -> RequestBuilder ================================================ FILE: integration/compose/src/main/java/com/bumptech/glide/integration/compose/Sizes.kt ================================================ @file:OptIn(InternalGlideApi::class) package com.bumptech.glide.integration.compose import androidx.compose.ui.unit.Constraints import com.bumptech.glide.RequestBuilder import com.bumptech.glide.integration.ktx.InternalGlideApi import com.bumptech.glide.integration.ktx.Size import com.bumptech.glide.integration.ktx.isValidGlideDimension import com.bumptech.glide.request.target.Target import kotlinx.coroutines.CompletableDeferred internal class SizeObserver { private val size = CompletableDeferred() fun setSize(size: Size) { this.size.complete(size) } suspend fun getSize(): Size { return size.await() } } internal fun RequestBuilder.overrideSize(): Size? = if (isOverrideSizeSet()) { Size(overrideWidth, overrideHeight) } else { null } internal fun RequestBuilder.isOverrideSizeSet(): Boolean = overrideWidth.isValidGlideDimension() && overrideHeight.isValidGlideDimension() internal fun Constraints.inferredGlideSize(): Size? { val width = if (hasBoundedWidth) maxWidth else Target.SIZE_ORIGINAL val height = if (hasBoundedHeight) maxHeight else Target.SIZE_ORIGINAL if (!width.isValidGlideDimension() || !height.isValidGlideDimension()) { return null } return Size(width, height) } ================================================ FILE: integration/compose/src/test/java/com/bumptech/glide/integration/compose/GlideImageTest.kt ================================================ package com.bumptech.glide.integration.compose import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.width import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @OptIn(ExperimentalGlideComposeApi::class) @RunWith(AndroidJUnit4::class) class GlideImageTest { @get:Rule(order = 1) val composeRule = createComposeRule() @Test fun glideImage_zeroWidthFillBounds_doesNotCrash() { composeRule.setContent { GlideImage( model = null, contentDescription = null, modifier = Modifier.width(0.dp).heightIn(0.dp, 100.dp), contentScale = ContentScale.FillBounds, loading = placeholder(android.R.drawable.star_on), ) } } } ================================================ FILE: integration/concurrent/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.concurrent" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) implementation(libs.guava) implementation(libs.androidx.futures) testImplementation(project(":mocks")) testImplementation(project(":testutil")) testImplementation(libs.androidx.test.core) testImplementation(libs.truth) testImplementation(libs.junit) testImplementation(libs.robolectric) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/concurrent/gradle.properties ================================================ POM_NAME=Glide Concurrent Integration POM_ARTIFACT_ID=concurrent-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library for using Glide with ListenableFutures ================================================ FILE: integration/concurrent/src/main/java/com/bumptech/glide/integration/concurrent/GlideFutures.java ================================================ package com.bumptech.glide.integration.concurrent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.concurrent.futures.CallbackToFutureAdapter; import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; import androidx.concurrent.futures.CallbackToFutureAdapter.Resolver; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.Executors; import com.google.common.base.Function; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.Executor; /** Utilities for getting ListenableFutures out of Glide. */ public final class GlideFutures { /** * Preloads the resource for {@code builder} and returns a {@link ListenableFuture} that can be * used to monitor status. * *

Shorthand for simply calling {@link #submitAndExecute(RequestManager, RequestBuilder, * ResourceConsumer, Executor)} with an empty {@code action}. */ // Wildcard resource types can't be directly instantiated, we don't need to care about the type // here. @SuppressWarnings({"rawtypes", "unchecked"}) public static ListenableFuture preload( final RequestManager requestManager, RequestBuilder builder, Executor executor) { return submitAndExecute( requestManager, builder, new ResourceConsumer() { @Override public void act(Object resource) {} }, executor); } /** * Acts on a resource loaded by Glide. * * @param The type of resource (Bitmap, Drawable etc). */ public interface ResourceConsumer { void act(T resource); } /** * Submits the provided request, performs the provided {@code action} and returns a {@link * ListenableFuture} that can be used to cancel the request or monitor its status. * *

Cancellation is best effort and may result in some resources not being returned back to * Glide's pool. In particular, if the request is cancelled after the resource is loaded by Glide, * but before {@code action} is run on {@code executor}, the resource will not be returned. We * have the unfortunate choice between unsafely returning resources to the pool immediately when * cancel is called while they may still be in use via {@link * com.google.common.util.concurrent.ClosingFuture} or occasionally failing to return resources to * the pool. Because failing to return resources to the pool is inefficient, but safe, that's the * route we've chosen. A more sophisticated implementation may allow us to avoid the resource * inefficiency. * *

If you do not need to interact with resource, use {@link #preload(RequestManager, * RequestBuilder, Executor)}. {@code preload} is more efficient because it knows that the * resource is never used and can always clear the resource immediately on cancellation, unlike * this method. * *

An example usage: * *

{@code
   *   ListenableFuture future =
   *     submitAndExecute(
   *       requestManager,
   *       requestBuilder,
   *       (bitmap) -> doSomethingWithBitmap(bitmap),
   *       backgroundExecutor);
   * ;
   * }
* * @param The type of resource that will be loaded (Bitmap, Drawable, etc). */ public static ListenableFuture submitAndExecute( final RequestManager requestManager, RequestBuilder requestBuilder, final ResourceConsumer action, Executor executor) { // If the request completes normally, then the target is cleared and the resource is returned. // If the request fails while loading the image, there's no need to clear. // If the request fails while calling the action, the target is cleared and the resource is // returned. // If the request is cancelled before the resource is loaded, then the resource is returned // If the request is cancelled after the resource is loaded but before the transform runs, // then the resource is dropped (but not leaked) // If the request is cancelled after the transform method starts, then the resource is returned. return FluentFuture.from(submitInternal(requestBuilder)) .transform( new Function, Void>() { @Override public Void apply(TargetAndResult targetAndResult) { try { action.act(targetAndResult.result); } finally { requestManager.clear(targetAndResult.target); } return null; } }, executor); } /** * Convert a pending load request into a ListenableFuture. * *

Sample code: * *

{@code
   * ListenableFuture image =
   *     GlideFutures.submit(requestManager.asFile().load(url));
   * }
* * @param requestBuilder A request builder for the resource to load. It must be tied to an * application Glide instance, and must not have a listener set. * @param The type of resource that will be loaded (Bitmap, Drawable, etc). */ public static ListenableFuture submit(final RequestBuilder requestBuilder) { return transformFromTargetAndResult(submitInternal(requestBuilder)); } private static ListenableFuture transformFromTargetAndResult( ListenableFuture> future) { return Futures.transform( future, new Function, T>() { @Override public T apply(TargetAndResult input) { return input.result; } }, Executors.directExecutor()); } private static ListenableFuture> submitInternal( final RequestBuilder requestBuilder) { return CallbackToFutureAdapter.getFuture( new Resolver>() { // Only used for toString @SuppressWarnings("FutureReturnValueIgnored") @Override public Object attachCompleter(@NonNull Completer> completer) { GlideLoadingListener listener = new GlideLoadingListener<>(completer); final FutureTarget futureTarget = requestBuilder.addListener(listener).submit(); completer.addCancellationListener( new Runnable() { @Override public void run() { futureTarget.cancel(/* mayInterruptIfRunning= */ true); } }, MoreExecutors.directExecutor()); return futureTarget; } }); } /** Listener to convert Glide load results into ListenableFutures. */ private static final class GlideLoadingListener implements RequestListener { private final Completer> completer; GlideLoadingListener(Completer> completer) { this.completer = completer; } @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirst) { completer.setException(e != null ? e : new RuntimeException("Unknown error")); return true; } @Override public boolean onResourceReady( @NonNull T resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirst) { try { completer.set(new TargetAndResult<>(target, resource)); } catch (Throwable t) { completer.setException(t); } return true; } } private static final class TargetAndResult { private final Target target; private final T result; TargetAndResult(Target target, T result) { this.target = target; this.result = result; } } private GlideFutures() {} } ================================================ FILE: integration/concurrent/src/test/java/com/bumptech/glide/integration/concurrent/GlideFuturesTest.java ================================================ package com.bumptech.glide.integration.concurrent; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.testutil.MockModelLoader; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.io.IOException; import java.util.concurrent.ExecutionException; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public final class GlideFuturesTest { private Context app; @Before public void setUp() { app = ApplicationProvider.getApplicationContext(); GlideExecutor executor = MockGlideExecutor.newMainThreadExecutor(); Glide.init( app, new GlideBuilder() .setAnimationExecutor(executor) .setSourceExecutor(executor) .setDiskCacheExecutor(executor)); } @Test public void testBaseLoad() throws Exception { ColorDrawable expected = new ColorDrawable(Color.RED); ListenableFuture future = GlideFutures.submit(Glide.with(app).load(expected)); assertThat(((ColorDrawable) Futures.getDone(future)).getColor()).isEqualTo(expected.getColor()); } @Test public void testErrorLoad() { // Load some unsupported model. final ListenableFuture future = GlideFutures.submit(Glide.with(app).asBitmap().load(app)); // Make sure that it throws. assertThrows( ExecutionException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { Futures.getDone(future); } }); } @Test public void testToString() throws Exception { Foo model = new Foo(); SettableFuture bar = SettableFuture.create(); Glide.get(app) .getRegistry() .prepend( Bar.class, Baz.class, new ResourceDecoder() { @Override public boolean handles(Bar source, Options options) throws IOException { return true; } @Override public Resource decode(Bar source, int width, int height, Options options) throws IOException { throw new IOException(); } }); MockModelLoader.mockAsync(model, Bar.class, bar); ListenableFuture future = GlideFutures.submit( Glide.with(app) .as(Baz.class) .load(model) .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE)); assertThat(future.toString()).contains("Foo"); future.cancel(true); assertThat(bar.isCancelled()).isTrue(); } private static final class Foo {} private static final class Bar {} private static final class Baz {} } ================================================ FILE: integration/cronet/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.cronet" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = 16 } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) implementation(libs.cronet) implementation(libs.guava) implementation(project(":annotation")) annotationProcessor(project(":annotation:compiler")) api(libs.androidx.annotation) testImplementation(libs.truth) testImplementation(libs.junit) testImplementation(libs.robolectric) testImplementation(libs.mockito.core) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/cronet/gradle.properties ================================================ POM_NAME=Glide Cronet Integration POM_ARTIFACT_ID=cronet-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library to use Cronet to fetch data over http/https in Glide ================================================ FILE: integration/cronet/lint.xml ================================================ ================================================ FILE: integration/cronet/src/main/AndroidManifest.xml ================================================ ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/BufferQueue.java ================================================ package com.bumptech.glide.integration.cronet; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; import org.chromium.net.UrlResponseInfo; /** * A utility for processing response bodies, as one contiguous buffer rather than an asynchronous * stream. */ final class BufferQueue { public static final String CONTENT_LENGTH = "content-length"; public static final String CONTENT_ENCODING = "content-encoding"; private static final int DEFAULT_BUFFER_SIZE = 16384; private final Queue buffers; private final AtomicBoolean isCoalesced = new AtomicBoolean(false); public static Builder builder() { return new Builder(); } /** * Use this class during a request, to combine streamed buffers of a response into a single final * buffer. * *

For example: {@code @Override public void onResponseStarted(UrlRequest request, * UrlResponseInfo info) { request.read(builder.getFirstBuffer(info)); } @Override public void * onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) { * request.read(builder.getNextBuffer(buffer)); } } */ public static final class Builder { private ArrayDeque buffers = new ArrayDeque<>(); private RuntimeException whenClosed; private Builder() {} /** Returns the next buffer to write data into. */ public ByteBuffer getNextBuffer(ByteBuffer lastBuffer) { if (buffers == null) { throw new RuntimeException(whenClosed); } if (lastBuffer != buffers.peekLast()) { buffers.addLast(lastBuffer); } if (lastBuffer.hasRemaining()) { return lastBuffer; } else { return ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE); } } /** Returns a ByteBuffer heuristically sized to hold the whole response body. */ public ByteBuffer getFirstBuffer(UrlResponseInfo info) { // Security note - a malicious server could attempt to exhaust client memory by sending // down a Content-Length of a very large size, which we would eagerly allocate without // the server having to actually send those bytes. This isn't considered to be an // issue, because that same malicious server could use our transparent gzip to force us // to allocate 1032 bytes per byte sent by the server. return ByteBuffer.allocateDirect((int) Math.min(bufferSizeHeuristic(info), 524288)); } @SuppressWarnings("checkstyle:UnnecessaryParentheses") // Readability private static long bufferSizeHeuristic(UrlResponseInfo info) { final Map> headers = info.getAllHeaders(); if (headers.containsKey(CONTENT_LENGTH)) { long contentLength = Long.parseLong(headers.get(CONTENT_LENGTH).get(0)); boolean isCompressed = !headers.containsKey(CONTENT_ENCODING) || (headers.get(CONTENT_ENCODING).size() == 1 && "identity".equals(headers.get(CONTENT_ENCODING).get(0))); if (isCompressed) { // We have to guess at the uncompressed size. In the future, consider guessing a // compression ratio based on the content-type and content-encoding. For now, // assume 2. return 2 * contentLength; } else { // In this case, we know exactly how many bytes we're going to get, so we can // size our buffer perfectly. However, we still have to call read() for the last time, // even when we know there shouldn't be any more bytes coming. To avoid allocating another // buffer for that case, add one more byte than we really need. return contentLength + 1; } } else { // No content-length. This means we're either being sent a chunked response, or the // java stack stripped content length because of transparent gzip. In either case we really // have no idea, and so we fall back to a reasonable guess. return DEFAULT_BUFFER_SIZE; } } public BufferQueue build() { whenClosed = new RuntimeException(); final ArrayDeque buffers = this.buffers; this.buffers = null; return new BufferQueue(buffers); } } private BufferQueue(Queue buffers) { this.buffers = buffers; for (ByteBuffer buffer : this.buffers) { buffer.flip(); } } /** Returns the response body as a single contiguous buffer. */ public ByteBuffer coalesceToBuffer() { markCoalesced(); if (buffers.size() == 0) { return ByteBuffer.allocateDirect(0); } else if (buffers.size() == 1) { return buffers.remove(); } else { int size = 0; for (ByteBuffer buffer : buffers) { size += buffer.remaining(); } ByteBuffer result = ByteBuffer.allocateDirect(size); while (!buffers.isEmpty()) { result.put(buffers.remove()); } result.flip(); return result; } } private void markCoalesced() { if (!isCoalesced.compareAndSet(false, true)) { throw new IllegalStateException("This BufferQueue has already been consumed"); } } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ByteBufferParser.java ================================================ package com.bumptech.glide.integration.cronet; import java.nio.ByteBuffer; /** * Parses a {@link java.nio.ByteBuffer} to a particular data type. * * @param The type of data to parse the buffer to. */ interface ByteBufferParser { /** Returns the required type of data parsed from the given {@link ByteBuffer}. */ T parse(ByteBuffer byteBuffer); /** Returns the {@link Class} of the data that will be parsed from {@link ByteBuffer}s. */ Class getDataClass(); } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ChromiumRequestSerializer.java ================================================ package com.bumptech.glide.integration.cronet; import android.util.Log; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.HttpException; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.GlideExecutor.UncaughtThrowableStrategy; import com.bumptech.glide.load.model.GlideUrl; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import java.io.IOException; import java.net.HttpURLConnection; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import org.chromium.net.CronetException; import org.chromium.net.UrlRequest; import org.chromium.net.UrlRequest.Callback; import org.chromium.net.UrlResponseInfo; /** * Ensures that two simultaneous requests for exactly the same url make only a single http request. * *

Requests are started by Glide on multiple threads in a thread pool. An arbitrary number of * threads may attempt to start or cancel requests for one or more urls at once. Our goal is to * ensure: *

  • * *
      * A new request is made to cronet if a url is requested and no cronet request for that url is * in progress *
    * *
      * Subsequent requests for in progress urls do not make new requests to cronet, but are notified * when the existing cronet request completes. *
    * *
      * Cancelling a single request does not cancel the cronet request if multiple requests for the url * have been made, but cancelling all requests for a url does cancel the cronet request. *
    */ final class ChromiumRequestSerializer { private static final String TAG = "ChromiumSerializer"; private static final Map GLIDE_TO_CHROMIUM_PRIORITY = new EnumMap<>(Priority.class); // Memoized so that all callers can share an instance. // Suppliers.memoize() is thread safe. See google3/java/com/google/common/base/Suppliers.java private static final Supplier GLIDE_EXECUTOR_SUPPLIER = Suppliers.memoize( new Supplier() { @Override public GlideExecutor get() { // Allow network operations, but use a single thread. See b/37684357. return GlideExecutor.newSourceExecutor( 1 /*threadCount*/, "chromium-serializer", UncaughtThrowableStrategy.DEFAULT); } }); private abstract static class PriorityRunnable implements Runnable, Comparable { private final int priority; private PriorityRunnable(Priority priority) { this.priority = priority.ordinal(); } @Override public final int compareTo(PriorityRunnable another) { if (another.priority > this.priority) { return -1; } else if (another.priority < this.priority) { return 1; } return 0; } } static { GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.IMMEDIATE, UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST); GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.HIGH, UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM); GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.NORMAL, UrlRequest.Builder.REQUEST_PRIORITY_LOW); GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.LOW, UrlRequest.Builder.REQUEST_PRIORITY_LOWEST); } private final JobPool jobPool; private final Map jobs = new HashMap<>(); private final CronetRequestFactory requestFactory; @Nullable private final DataLogger dataLogger; ChromiumRequestSerializer( CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger, @Nullable final GlideExecutor executor) { this.requestFactory = requestFactory; this.dataLogger = dataLogger; if (executor == null) { this.jobPool = new JobPool(GLIDE_EXECUTOR_SUPPLIER); } else { this.jobPool = new JobPool( new Supplier() { @Override public Executor get() { return executor; } }); } } void startRequest(Priority priority, GlideUrl glideUrl, Listener listener) { boolean startNewRequest = false; Job job; synchronized (this) { job = jobs.get(glideUrl); if (job == null) { startNewRequest = true; job = jobPool.get(glideUrl); jobs.put(glideUrl, job); } job.addListener(listener); } if (startNewRequest) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Fetching image url using cronet" + " url: " + glideUrl); } job.priority = priority; job.request = requestFactory .newRequest( glideUrl.toStringUrl(), GLIDE_TO_CHROMIUM_PRIORITY.get(priority), glideUrl.getHeaders(), job) .build(); job.request.start(); // It's possible we will be cancelled between adding the job to the job list and starting the // corresponding request. We don't want to hold a lock while starting the request, because // starting the request may block for a while and we need cancellation to happen quickly (it // happens on the main thread). if (job.isCancelled) { job.request.cancel(); } } } void cancelRequest(GlideUrl glideUrl, Listener listener) { final Job job; synchronized (this) { job = jobs.get(glideUrl); } // Jobs may be cancelled before they are started. if (job != null) { job.removeListener(listener); } } private static IOException getExceptionIfFailed( UrlResponseInfo info, IOException e, boolean wasCancelled) { if (wasCancelled) { return null; } else if (e != null) { return e; } else if (info.getHttpStatusCode() != HttpURLConnection.HTTP_OK) { return new HttpException(info.getHttpStatusCode()); } return null; } /** * Manages a single cronet request for a single url with one or more active listeners. * *

    Cronet requests are cancelled when all listeners are removed. */ private class Job extends Callback { private final List listeners = new ArrayList<>(2); private GlideUrl glideUrl; private Priority priority; private long startTime; private UrlRequest request; private long endTimeMs; private long responseStartTimeMs; private volatile boolean isCancelled; private BufferQueue.Builder builder; private final Supplier executorSupplier; Job(Supplier executorSupplier) { this.executorSupplier = executorSupplier; } void init(GlideUrl glideUrl) { startTime = System.currentTimeMillis(); this.glideUrl = glideUrl; } void addListener(Listener listener) { synchronized (ChromiumRequestSerializer.this) { listeners.add(listener); } } void removeListener(Listener listener) { synchronized (ChromiumRequestSerializer.this) { // Note: multiple cancellation calls + a subsequent request for a url may mean we fail to // remove the listener here because that listener is actually for a previous request. Since // that race is harmless, we simply ignore it. listeners.remove(listener); if (listeners.isEmpty()) { isCancelled = true; jobs.remove(glideUrl); } } // The request may not have started yet, so request may be null. if (isCancelled) { UrlRequest localRequest = request; if (localRequest != null) { localRequest.cancel(); } } } @Override public void onRedirectReceived(UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, String s) throws Exception { urlRequest.followRedirect(); } @Override public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { responseStartTimeMs = System.currentTimeMillis(); builder = BufferQueue.builder(); request.read(builder.getFirstBuffer(info)); } @Override public void onReadCompleted( UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, ByteBuffer byteBuffer) throws Exception { request.read(builder.getNextBuffer(byteBuffer)); } @Override public void onSucceeded(UrlRequest request, final UrlResponseInfo info) { executorSupplier .get() .execute( new PriorityRunnable(priority) { @Override public void run() { onRequestFinished( info, null /*exception*/, false /*wasCancelled*/, builder.build().coalesceToBuffer()); } }); } @Override public void onFailed( UrlRequest urlRequest, final UrlResponseInfo urlResponseInfo, final CronetException e) { executorSupplier .get() .execute( new PriorityRunnable(priority) { @Override public void run() { onRequestFinished(urlResponseInfo, e, false /*wasCancelled*/, null /*buffer*/); } }); } @Override public void onCanceled(UrlRequest urlRequest, @Nullable final UrlResponseInfo urlResponseInfo) { executorSupplier .get() .execute( new PriorityRunnable(priority) { @Override public void run() { onRequestFinished( urlResponseInfo, null /*exception*/, true /*wasCancelled*/, null /*buffer*/); } }); } private void onRequestFinished( UrlResponseInfo info, @Nullable CronetException e, boolean wasCancelled, ByteBuffer buffer) { synchronized (ChromiumRequestSerializer.this) { jobs.remove(glideUrl); } Exception exception = getExceptionIfFailed(info, e, wasCancelled); boolean isSuccess = exception == null && !wasCancelled; endTimeMs = System.currentTimeMillis(); maybeLogResult(isSuccess, exception, wasCancelled, buffer); if (isSuccess) { notifySuccess(buffer); } else { notifyFailure(exception); } if (dataLogger != null) { dataLogger.logNetworkData(info, startTime, responseStartTimeMs, endTimeMs); } builder = null; jobPool.put(this); } private void notifySuccess(ByteBuffer buffer) { ByteBuffer toNotify = buffer; /* Locking here isn't necessary and is potentially dangerous. There's an optimization in * Glide that avoids re-posting results if the callback onRequestComplete triggers is called * on the calling thread. If that were ever to happen here (the request is cached in memory?), * this might block all requests for a while. Locking isn't necessary because the Job is * removed from the serializer's job set at the beginning of onRequestFinished. After that * point, whatever thread we're on is the only one that has access to the Job. Subsequent * requests for the same image would trigger an additional RPC/Job. */ for (int i = 0, size = listeners.size(); i < size; i++) { Listener listener = listeners.get(i); listener.onRequestComplete(toNotify); toNotify = (ByteBuffer) toNotify.asReadOnlyBuffer().position(0); } } private void notifyFailure(Exception exception) { /* Locking here isn't necessary and is potentially dangerous. There's an optimization in * Glide that avoids re-posting results if the callback onRequestComplete triggers is called * on the calling thread. If that were ever to happen here (the request is cached in memory?), * this might block all requests for a while. Locking isn't necessary because the Job is * removed from the serializer's job set at the beginning of onRequestFinished. After that * point, whatever thread we're on is the only one that has access to the Job. Subsequent * requests for the same image would trigger an additional RPC/Job. */ for (int i = 0, size = listeners.size(); i < size; i++) { Listener listener = listeners.get(i); listener.onRequestFailed(exception); } } private void maybeLogResult( boolean isSuccess, Exception exception, boolean wasCancelled, ByteBuffer buffer) { if (isSuccess && Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Successfully completed request" + ", url: " + glideUrl + ", duration: " + (System.currentTimeMillis() - startTime) + ", file size: " + (buffer.limit() / 1024) + "kb"); } else if (!isSuccess && Log.isLoggable(TAG, Log.ERROR) && !wasCancelled) { Log.e(TAG, "Request failed, url: " + glideUrl, exception); } } private void clearListeners() { synchronized (ChromiumRequestSerializer.this) { listeners.clear(); request = null; isCancelled = false; } } } private class JobPool { private static final int MAX_POOL_SIZE = 50; private final ArrayDeque pool = new ArrayDeque<>(); private final Supplier executorSupplier; public JobPool(Supplier executorSupplier) { this.executorSupplier = executorSupplier; } public synchronized Job get(GlideUrl glideUrl) { Job job = pool.poll(); if (job == null) { job = new Job(executorSupplier); } job.init(glideUrl); return job; } public void put(Job job) { job.clearListeners(); synchronized (this) { if (pool.size() < MAX_POOL_SIZE) { pool.offer(job); } } } } interface Listener { void onRequestComplete(ByteBuffer byteBuffer); void onRequestFailed(@Nullable Exception e); } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ChromiumUrlFetcher.java ================================================ package com.bumptech.glide.integration.cronet; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import java.nio.ByteBuffer; /** An {@link DataFetcher} for fetching {@link GlideUrl} using cronet. */ final class ChromiumUrlFetcher implements DataFetcher, ChromiumRequestSerializer.Listener { private final ChromiumRequestSerializer serializer; private final ByteBufferParser parser; private final GlideUrl url; private DataCallback callback; public ChromiumUrlFetcher( ChromiumRequestSerializer serializer, ByteBufferParser parser, GlideUrl url) { this.serializer = serializer; this.parser = parser; this.url = url; } @Override public void loadData(Priority priority, DataCallback callback) { this.callback = callback; serializer.startRequest(priority, url, this); } @Override public void cleanup() { // Nothing to cleanup. } @Override public void cancel() { serializer.cancelRequest(url, this); } @Override public Class getDataClass() { return parser.getDataClass(); } @Override public DataSource getDataSource() { return DataSource.REMOTE; } @Override public void onRequestComplete(ByteBuffer byteBuffer) { callback.onDataReady(parser.parse(byteBuffer)); } @Override public void onRequestFailed(@Nullable Exception e) { callback.onLoadFailed(e); } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/ChromiumUrlLoader.java ================================================ package com.bumptech.glide.integration.cronet; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.util.ByteBufferUtil; import java.io.InputStream; import java.nio.ByteBuffer; /** * A {@link com.bumptech.glide.load.model.ModelLoader} for loading urls using cronet. * *

    You can optionally pass an executor to the constructor for handling cronet callbacks in {@link * ChromiumRequestSerializer}. If the executor is not provided, it will be created for you. * * @param The type of data this loader will load. */ public final class ChromiumUrlLoader implements ModelLoader { private final ChromiumRequestSerializer requestSerializer; private final ByteBufferParser parser; ChromiumUrlLoader(CronetRequestFactory requestFactory, ByteBufferParser parser) { this(parser, requestFactory, null /*dataLogger*/); } ChromiumUrlLoader( ByteBufferParser parser, CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger) { this.parser = parser; requestSerializer = new ChromiumRequestSerializer(requestFactory, dataLogger, /* executor= */ null); } ChromiumUrlLoader( ByteBufferParser parser, CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger, @Nullable GlideExecutor executor) { this.parser = parser; requestSerializer = new ChromiumRequestSerializer(requestFactory, dataLogger, executor); } @Override public LoadData buildLoadData(GlideUrl glideUrl, int width, int height, Options options) { DataFetcher fetcher = new ChromiumUrlFetcher<>(requestSerializer, parser, glideUrl); return new LoadData<>(glideUrl, fetcher); } @Override public boolean handles(GlideUrl glideUrl) { return true; } /** Loads {@link InputStream}s for {@link GlideUrl}s using cronet. */ public static final class StreamFactory implements ModelLoaderFactory, ByteBufferParser { private CronetRequestFactory requestFactory; @Nullable private final DataLogger dataLogger; @Nullable private final GlideExecutor executor; public StreamFactory(CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger) { this.requestFactory = requestFactory; this.dataLogger = dataLogger; this.executor = null; } /** * @param executor See {@link ChromiumUrlLoader} for details. */ public StreamFactory( CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger, @Nullable GlideExecutor executor) { this.requestFactory = requestFactory; this.dataLogger = dataLogger; this.executor = executor; } @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ChromiumUrlLoader<>(/* parser= */ this, requestFactory, dataLogger, executor); } @Override public void teardown() {} @Override public InputStream parse(ByteBuffer byteBuffer) { return ByteBufferUtil.toStream(byteBuffer); } @Override public Class getDataClass() { return InputStream.class; } } /** Loads {@link ByteBuffer}s for {@link GlideUrl}s using cronet. */ public static final class ByteBufferFactory implements ModelLoaderFactory, ByteBufferParser { private CronetRequestFactory requestFactory; @Nullable private final DataLogger dataLogger; @Nullable private final GlideExecutor executor; public ByteBufferFactory(CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger) { this.requestFactory = requestFactory; this.dataLogger = dataLogger; this.executor = null; } /** * @param executor See {@link ChromiumUrlLoader} for details. */ public ByteBufferFactory( CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger, @Nullable GlideExecutor executor) { this.requestFactory = requestFactory; this.dataLogger = dataLogger; this.executor = executor; } @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ChromiumUrlLoader<>(/* parser= */ this, requestFactory, dataLogger, executor); } @Override public void teardown() { // Do nothing. } @Override public ByteBuffer parse(ByteBuffer byteBuffer) { return byteBuffer; } @Override public Class getDataClass() { return ByteBuffer.class; } } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/CronetEngineSingleton.java ================================================ package com.bumptech.glide.integration.cronet; import android.content.Context; import org.chromium.net.CronetEngine; /** * Class controlling singleton instance of the CronetEngine. Ensures at most one instance of the * CronetEngine is created. */ // NOTE: This is a standalone class and not a memoized supplier as the CronetEngine creations // requires a parameter, namedly a Context reference. public final class CronetEngineSingleton { // non instantiable private CronetEngineSingleton() {} private static volatile CronetEngine cronetEngineSingleton; public static CronetEngine getSingleton(Context context) { // Lazily create the engine. if (cronetEngineSingleton == null) { synchronized (CronetEngineSingleton.class) { // have to re-check since this might have changed before synchronization, but we don't // want to synchronize just to check for null. if (cronetEngineSingleton == null) { cronetEngineSingleton = createEngine(context); } } } return cronetEngineSingleton; } private static CronetEngine createEngine(Context context) { return new CronetEngine.Builder(context) .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISABLED, 0) .enableHttp2(true) .enableQuic(false) .build(); } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/CronetGlideModule.java ================================================ package com.bumptech.glide.integration.cronet; import android.content.Context; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.GlideModule; import com.google.common.base.Supplier; import java.io.InputStream; import java.nio.ByteBuffer; import org.chromium.net.CronetEngine; /** * A {@link GlideModule} that registers components allowing remote image fetching to be done using * Cronet. */ public final class CronetGlideModule implements GlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) {} @Override public void registerComponents(final Context context, Glide glide, Registry registry) { CronetRequestFactory factory = new CronetRequestFactoryImpl( new Supplier() { @Override public CronetEngine get() { return CronetEngineSingleton.getSingleton(context); } }); registry.replace( GlideUrl.class, InputStream.class, new ChromiumUrlLoader.StreamFactory(factory, null)); registry.prepend( GlideUrl.class, ByteBuffer.class, new ChromiumUrlLoader.ByteBufferFactory(factory, null)); } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/CronetLibraryGlideModule.java ================================================ package com.bumptech.glide.integration.cronet; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.integration.cronet.ChromiumUrlLoader.ByteBufferFactory; import com.bumptech.glide.integration.cronet.ChromiumUrlLoader.StreamFactory; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.LibraryGlideModule; import com.google.common.base.Supplier; import java.io.InputStream; import java.nio.ByteBuffer; import org.chromium.net.CronetEngine; /** * A {@link LibraryGlideModule} that registers components allowing remote image fetching to be done * using Cronet. */ @GlideModule public final class CronetLibraryGlideModule extends LibraryGlideModule { @Override public void registerComponents( final @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { CronetRequestFactory factory = new CronetRequestFactoryImpl( new Supplier() { @Override public CronetEngine get() { return CronetEngineSingleton.getSingleton(context); } }); registry.replace( GlideUrl.class, InputStream.class, new StreamFactory(factory, null /* dataLogger */)); registry.prepend( GlideUrl.class, ByteBuffer.class, new ByteBufferFactory(factory, null /* dataLogger */)); } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/CronetRequestFactory.java ================================================ package com.bumptech.glide.integration.cronet; import java.util.Map; import org.chromium.net.UrlRequest; /** Factory to build custom cronet requests. */ public interface CronetRequestFactory { UrlRequest.Builder newRequest( String url, int requestPriority, Map headers, UrlRequest.Callback listener); } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/CronetRequestFactoryImpl.java ================================================ package com.bumptech.glide.integration.cronet; import com.google.common.base.Supplier; import java.util.Map; import java.util.concurrent.Executor; import org.chromium.net.CronetEngine; import org.chromium.net.UrlRequest; /** Default implementation for building cronet requests. */ public final class CronetRequestFactoryImpl implements CronetRequestFactory { private final Supplier cronetEngineGetter; public CronetRequestFactoryImpl(Supplier cronetEngineGetter) { this.cronetEngineGetter = cronetEngineGetter; } @Override public UrlRequest.Builder newRequest( String url, int requestPriority, Map headers, UrlRequest.Callback listener) { CronetEngine engine = cronetEngineGetter.get(); UrlRequest.Builder builder = engine.newUrlRequestBuilder( url, listener, new Executor() { @Override public void execute(Runnable runnable) { runnable.run(); } }); builder.allowDirectExecutor(); builder.setPriority(requestPriority); for (Map.Entry header : headers.entrySet()) { // Cronet owns the Accept-Encoding header and user agent String key = header.getKey(); if ("Accept-Encoding".equalsIgnoreCase(key) || "User-Agent".equalsIgnoreCase(key)) { continue; } builder.addHeader(key, header.getValue()); } return builder; } } ================================================ FILE: integration/cronet/src/main/java/com/bumptech/glide/integration/cronet/DataLogger.java ================================================ package com.bumptech.glide.integration.cronet; import androidx.annotation.Nullable; import org.chromium.net.UrlResponseInfo; /** A interface for logging data information related to loading the data. */ public interface DataLogger { /** * Logs the related network information. * * @param httpUrlRequest HttpUrlRequest that contains information on the request. May be {@code * null} if the request was cancelled. * @param startTimeMs Timestamp (ms) that the request started. * @param responseStartTimeMs Timestamp (ms) when the first header byte was received. * @param endTimeMs Timestamp (ms) that the request ended. */ void logNetworkData( @Nullable UrlResponseInfo httpUrlRequest, long startTimeMs, long responseStartTimeMs, long endTimeMs); } ================================================ FILE: integration/cronet/src/test/AndroidManifest.xml ================================================ ================================================ FILE: integration/cronet/src/test/java/com/bumptech/glide/integration/cronet/ChromiumUrlFetcherTest.java ================================================ package com.bumptech.glide.integration.cronet; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.HttpException; import com.bumptech.glide.load.data.DataFetcher.DataCallback; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.LazyHeaders; import com.bumptech.glide.load.model.LazyHeaders.Builder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; import java.net.HttpURLConnection; import java.nio.ByteBuffer; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import org.chromium.net.CronetEngine; import org.chromium.net.CronetException; import org.chromium.net.UrlRequest; import org.chromium.net.UrlRequest.Callback; import org.chromium.net.UrlResponseInfo; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; /** Tests for {@link ChromiumUrlFetcher}. */ @RunWith(RobolectricTestRunner.class) public class ChromiumUrlFetcherTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Mock private DataCallback callback; @Mock private CronetEngine cronetEngine; @Mock private UrlRequest request; @Mock private UrlRequest.Builder mockUrlRequestBuilder; @Mock private ByteBufferParser parser; @Mock private CronetRequestFactory cronetRequestFactory; @Mock private DataCallback firstCallback; @Mock private DataCallback secondCallback; private UrlRequest.Builder builder; private GlideUrl glideUrl; private ChromiumUrlFetcher fetcher; private ChromiumRequestSerializer serializer; private ArgumentCaptor urlRequestListenerCaptor; @Before public void setUp() { when(parser.getDataClass()).thenReturn(ByteBuffer.class); when(parser.parse(any(ByteBuffer.class))) .thenAnswer( new Answer() { @Override public ByteBuffer answer(InvocationOnMock invocation) throws Throwable { return (ByteBuffer) invocation.getArguments()[0]; } }); when(cronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) .thenReturn(mockUrlRequestBuilder); when(mockUrlRequestBuilder.build()).thenReturn(request); glideUrl = new GlideUrl("http://www.google.com"); urlRequestListenerCaptor = ArgumentCaptor.forClass(UrlRequest.Callback.class); serializer = new ChromiumRequestSerializer( cronetRequestFactory, /* dataLogger= */ null, /* executor= */ null); fetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); builder = cronetEngine.newUrlRequestBuilder( glideUrl.toStringUrl(), mock(UrlRequest.Callback.class), MoreExecutors.directExecutor()); when(cronetRequestFactory.newRequest( anyString(), anyInt(), anyHeaders(), urlRequestListenerCaptor.capture())) .thenReturn(builder); when(builder.build()).thenReturn(request); } @Test public void testLoadData_createsAndStartsRequest() { when(cronetRequestFactory.newRequest( eq(glideUrl.toStringUrl()), eq(UrlRequest.Builder.REQUEST_PRIORITY_LOWEST), anyHeaders(), any(UrlRequest.Callback.class))) .thenReturn(builder); fetcher.loadData(Priority.LOW, callback); verify(request).start(); } @Test public void testLoadData_providesHeadersFromGlideUrl() { LazyHeaders.Builder headersBuilder = new Builder(); headersBuilder.addHeader("key", "value"); LazyHeaders headers = headersBuilder.build(); glideUrl = new GlideUrl("http://www.google.com", headers); fetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); fetcher.loadData(Priority.LOW, callback); verify(cronetRequestFactory) .newRequest( ArgumentMatchers.eq(glideUrl.toStringUrl()), anyInt(), ArgumentMatchers.eq(headers.getHeaders()), any(UrlRequest.Callback.class)); verify(request).start(); } @Test public void testLoadData_withInProgressRequest_doesNotStartNewRequest() { ChromiumUrlFetcher firstFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); ChromiumUrlFetcher secondFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); firstFetcher.loadData(Priority.LOW, callback); secondFetcher.loadData(Priority.HIGH, callback); verify(cronetRequestFactory, times(1)) .newRequest( ArgumentMatchers.eq(glideUrl.toStringUrl()), anyInt(), ArgumentMatchers.anyMap(), any(UrlRequest.Callback.class)); } @Test public void testLoadData_withInProgressRequest_isNotifiedWhenRequestCompletes() throws Exception { ChromiumUrlFetcher firstFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); ChromiumUrlFetcher secondFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); firstFetcher.loadData(Priority.LOW, firstCallback); secondFetcher.loadData(Priority.HIGH, secondCallback); succeed(getInfo(10, 200), urlRequestListenerCaptor.getValue(), ByteBuffer.allocateDirect(10)); verify(firstCallback, timeout(1000)).onDataReady(isA(ByteBuffer.class)); verify(secondCallback, timeout(1000)).onDataReady(isA(ByteBuffer.class)); } @NonNull private UrlResponseInfo getInfo(final int contentLength, final int statusCode) { return new UrlResponseInfo() { @Override public String getUrl() { return glideUrl.toStringUrl(); } @Override public List getUrlChain() { return ImmutableList.of(getUrl()); } @Override public int getHttpStatusCode() { return statusCode; } @Override public String getHttpStatusText() { return "OK"; } @Override public List> getAllHeadersAsList() { return ImmutableList.>of( new SimpleImmutableEntry<>("Content-Length", Integer.toString(contentLength))); } @Override public Map> getAllHeaders() { ImmutableMap.Builder> builder = ImmutableMap.builder(); for (Map.Entry entry : getAllHeadersAsList()) { builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue().split(","))); } return builder.build(); } @Override public boolean wasCached() { return false; } @Override public String getNegotiatedProtocol() { return ""; } @Override public String getProxyServer() { return ""; } @Override public long getReceivedByteCount() { return 0; } }; } @Test public void testCancel_withMultipleInProgressRequests_doesNotCancelChromiumRequest() { ChromiumUrlFetcher firstFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); ChromiumUrlFetcher secondFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); firstFetcher.loadData(Priority.LOW, callback); secondFetcher.loadData(Priority.HIGH, callback); firstFetcher.cancel(); verify(request, never()).cancel(); } @Test public void testCancel_afterCancellingAllInProgressRequests_cancelsChromiumRequest() { ChromiumUrlFetcher firstFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); ChromiumUrlFetcher secondFetcher = new ChromiumUrlFetcher<>(serializer, parser, glideUrl); firstFetcher.loadData(Priority.LOW, callback); secondFetcher.loadData(Priority.HIGH, callback); firstFetcher.cancel(); secondFetcher.cancel(); verify(request).cancel(); } @Test public void testCancel_withNoStartedRequest_doesNothing() { fetcher.cancel(); } @Test public void testCancel_withStartedRequest_cancelsRequest() { fetcher.loadData(Priority.LOW, callback); fetcher.cancel(); verify(request).cancel(); } @Test public void testRequestComplete_withNonNullException_callsCallbackWithException() { CronetException expected = new CronetException("test", /* cause= */ null) { static final long serialVersionUID = 1; }; fetcher.loadData(Priority.LOW, callback); urlRequestListenerCaptor.getValue().onFailed(request, null, expected); verify(callback, timeout(1000)).onLoadFailed(eq(expected)); } @Test public void testRequestComplete_withNon200StatusCode_callsCallbackWithException() throws Exception { UrlResponseInfo info = getInfo(0, HttpURLConnection.HTTP_INTERNAL_ERROR); fetcher.loadData(Priority.LOW, callback); UrlRequest.Callback urlCallback = urlRequestListenerCaptor.getValue(); succeed(info, urlCallback, ByteBuffer.allocateDirect(0)); ArgumentCaptor captor = ArgumentCaptor.forClass(HttpException.class); verify(callback, timeout(1000)).onLoadFailed(captor.capture()); assertThat(captor.getValue()) .hasMessageThat() .isEqualTo("Http request failed, status code: 500"); } private void succeed(UrlResponseInfo info, Callback urlCallback, ByteBuffer byteBuffer) throws Exception { byteBuffer.position(byteBuffer.limit()); urlCallback.onResponseStarted(request, info); urlCallback.onReadCompleted(request, info, byteBuffer); urlCallback.onSucceeded(request, info); } @Test public void testRequestComplete_withUnauthorizedStatusCode_callsCallbackWithAuthError() throws Exception { UrlResponseInfo info = getInfo(0, HttpURLConnection.HTTP_FORBIDDEN); fetcher.loadData(Priority.LOW, callback); UrlRequest.Callback urlCallback = urlRequestListenerCaptor.getValue(); succeed(info, urlCallback, ByteBuffer.allocateDirect(0)); verifyAuthError(); } @Test public void testRequestComplete_whenCancelledAndUnauthorized_callsCallbackWithNullError() throws Exception { UrlResponseInfo info = getInfo(0, HttpURLConnection.HTTP_FORBIDDEN); fetcher.loadData(Priority.HIGH, callback); Callback urlCallback = urlRequestListenerCaptor.getValue(); urlCallback.onResponseStarted(request, info); urlCallback.onCanceled(request, info); verify(callback, timeout(1000)).onLoadFailed(ArgumentMatchers.isNull()); } private void verifyAuthError() { ArgumentCaptor exceptionArgumentCaptor = ArgumentCaptor.forClass(Exception.class); verify(callback, timeout(1000)).onLoadFailed(exceptionArgumentCaptor.capture()); HttpException exception = (HttpException) exceptionArgumentCaptor.getValue(); assertThat(exception.getStatusCode()).isEqualTo(HttpURLConnection.HTTP_FORBIDDEN); } @Test public void testRequestComplete_with200AndCancelled_callsCallbackWithNullException() throws Exception { UrlResponseInfo info = getInfo(0, 200); fetcher.loadData(Priority.LOW, callback); Callback urlCallback = urlRequestListenerCaptor.getValue(); urlCallback.onResponseStarted(request, info); urlCallback.onCanceled(request, info); verify(callback, timeout(1000)).onLoadFailed(ArgumentMatchers.isNull()); } @Test public void testRequestComplete_with200NotCancelledMatchingLength_callsCallbackWithValidData() throws Exception { String data = "data"; ByteBuffer expected = ByteBuffer.wrap(data.getBytes()); ArgumentCaptor captor = ArgumentCaptor.forClass(ByteBuffer.class); fetcher.loadData(Priority.LOW, callback); succeed( getInfo(expected.remaining(), 200), urlRequestListenerCaptor.getValue(), expected.duplicate()); verify(callback, timeout(1000)).onDataReady(captor.capture()); ByteBuffer received = captor.getValue(); assertThat( new String( received.array(), received.arrayOffset() + received.position(), received.remaining())) .isEqualTo(data); } private static Map anyHeaders() { return anyMap(); } } ================================================ FILE: integration/gifencoder/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.gifencoder" compileSdkVersion = libs.versions.compile.sdk.version.get() sourceSets { getByName("main") { java.srcDirs("src/main/java", "../../third_party/gif_encoder/src/main/java") } } defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) testImplementation(project(":testutil")) testImplementation(libs.truth) testImplementation(libs.junit) testImplementation(libs.mockito.core) testImplementation(libs.robolectric) testImplementation(libs.androidx.test.core) testImplementation(libs.androidx.junit) testImplementation(libs.androidx.test.runner) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/gifencoder/gradle.properties ================================================ POM_NAME=Glide GifEncoder Integration POM_ARTIFACT_ID=gifencoder-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library allowing users to re-encode or create animated GIFs ================================================ FILE: integration/gifencoder/lint.xml ================================================ ================================================ FILE: integration/gifencoder/src/main/java/com/bumptech/glide/integration/gifencoder/ReEncodingGifResourceEncoder.java ================================================ package com.bumptech.glide.integration.gifencoder; import android.content.Context; import android.graphics.Bitmap; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.gifdecoder.GifHeader; import com.bumptech.glide.gifdecoder.GifHeaderParser; import com.bumptech.glide.gifdecoder.StandardGifDecoder; import com.bumptech.glide.gifencoder.AnimatedGifEncoder; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.UnitTransformation; import com.bumptech.glide.load.resource.bitmap.BitmapResource; import com.bumptech.glide.load.resource.gif.GifBitmapProvider; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.util.ByteBufferUtil; import com.bumptech.glide.util.LogTime; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.security.MessageDigest; /** * An {@link com.bumptech.glide.load.ResourceEncoder} that can write {@link * com.bumptech.glide.load.resource.gif.GifDrawable} to cache. */ public class ReEncodingGifResourceEncoder implements ResourceEncoder { private static final String KEY_ENCODE_TRANSFORMATION = "com.bumptech.glide.load.resource.gif.GifResourceEncoder.EncodeTransformation"; /** * A boolean option that, if set to true, causes the fully transformed GIF to be * written to cache. * *

    Warning - encoding GIFs is slow and often produces larger and less efficient GIFs than the * originals. Re-encoding may be worth it to decrease the size of very large GIFs. * *

    Defaults to false. */ // Public API. @SuppressWarnings("WeakerAccess") public static final Option ENCODE_TRANSFORMATION = Option.disk( KEY_ENCODE_TRANSFORMATION, false, new Option.CacheKeyUpdater() { @Override public void update( @NonNull byte[] keyBytes, @NonNull Boolean value, @NonNull MessageDigest messageDigest) { if (value) { messageDigest.update(keyBytes); } } }); private static final Factory FACTORY = new Factory(); private static final String TAG = "GifEncoder"; private final GifDecoder.BitmapProvider provider; private final Context context; private final BitmapPool bitmapPool; private final Factory factory; // Public API. @SuppressWarnings("unused") public ReEncodingGifResourceEncoder(@NonNull Context context, @NonNull BitmapPool bitmapPool) { this(context, bitmapPool, FACTORY); } @VisibleForTesting ReEncodingGifResourceEncoder(Context context, BitmapPool bitmapPool, Factory factory) { this.context = context; this.bitmapPool = bitmapPool; provider = new GifBitmapProvider(bitmapPool); this.factory = factory; } @NonNull @Override public EncodeStrategy getEncodeStrategy(@NonNull Options options) { Boolean encodeTransformation = options.get(ENCODE_TRANSFORMATION); return encodeTransformation != null && encodeTransformation ? EncodeStrategy.TRANSFORMED : EncodeStrategy.SOURCE; } @Override public boolean encode( @NonNull Resource resource, @NonNull File file, @NonNull Options options) { GifDrawable drawable = resource.get(); Transformation transformation = drawable.getFrameTransformation(); boolean isTransformed = !(transformation instanceof UnitTransformation); if (isTransformed && options.get(ENCODE_TRANSFORMATION)) { return encodeTransformedToFile(drawable, file); } else { return writeDataDirect(drawable.getBuffer(), file); } } private boolean encodeTransformedToFile(GifDrawable drawable, File file) { long startTime = LogTime.getLogTime(); OutputStream os = null; boolean success = false; try { os = new BufferedOutputStream(new FileOutputStream(file)); success = encodeTransformedToStream(drawable, os); os.close(); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to encode GIF", e); } } finally { if (os != null) { try { os.close(); } catch (IOException e) { // Ignored. } } } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Re-encoded GIF with " + drawable.getFrameCount() + " frames and " + drawable.getBuffer().limit() + " bytes in " + LogTime.getElapsedMillis(startTime) + " ms"); } return success; } private boolean encodeTransformedToStream(GifDrawable drawable, OutputStream os) { Transformation transformation = drawable.getFrameTransformation(); GifDecoder decoder = decodeHeaders(drawable.getBuffer()); AnimatedGifEncoder encoder = factory.buildEncoder(); if (!encoder.start(os)) { return false; } for (int i = 0; i < decoder.getFrameCount(); i++) { Bitmap currentFrame = decoder.getNextFrame(); Resource transformedResource = getTransformedFrame(currentFrame, transformation, drawable); try { if (!encoder.addFrame(transformedResource.get())) { return false; } int currentFrameIndex = decoder.getCurrentFrameIndex(); int delay = decoder.getDelay(currentFrameIndex); encoder.setDelay(delay); decoder.advance(); } finally { transformedResource.recycle(); } } return encoder.finish(); } private boolean writeDataDirect(ByteBuffer data, File file) { try { ByteBufferUtil.toFile(data, file); } catch (IOException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to write GIF data", e); } return false; } return true; } private GifDecoder decodeHeaders(ByteBuffer data) { GifHeaderParser parser = factory.buildParser(); parser.setData(data); GifHeader header = parser.parseHeader(); GifDecoder decoder = factory.buildDecoder(provider); decoder.setData(header, data); decoder.advance(); return decoder; } private Resource getTransformedFrame( Bitmap currentFrame, Transformation transformation, GifDrawable drawable) { // TODO: what if current frame is null? Resource bitmapResource = factory.buildFrameResource(currentFrame, bitmapPool); Resource transformedResource = transformation.transform( context, bitmapResource, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); if (!bitmapResource.equals(transformedResource)) { bitmapResource.recycle(); } return transformedResource; } @VisibleForTesting static class Factory { GifDecoder buildDecoder(GifDecoder.BitmapProvider bitmapProvider) { return new StandardGifDecoder(bitmapProvider); } GifHeaderParser buildParser() { return new GifHeaderParser(); } AnimatedGifEncoder buildEncoder() { return new AnimatedGifEncoder(); } @NonNull Resource buildFrameResource(@NonNull Bitmap bitmap, @NonNull BitmapPool bitmapPool) { return new BitmapResource(bitmap, bitmapPool); } } } ================================================ FILE: integration/gifencoder/src/test/java/com/bumptech/glide/integration/gifencoder/ReEncodingGifResourceEncoderTest.java ================================================ package com.bumptech.glide.integration.gifencoder; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.content.Context; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.gifdecoder.GifHeader; import com.bumptech.glide.gifdecoder.GifHeaderParser; import com.bumptech.glide.gifencoder.AnimatedGifEncoder; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.UnitTransformation; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.util.ByteBufferUtil; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; /** Tests for {@link com.bumptech.glide.integration.gifencoder.ReEncodingGifResourceEncoder}. */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE, sdk = 19) public class ReEncodingGifResourceEncoderTest { @Mock private Resource resource; @Mock private GifDecoder decoder; @Mock private GifHeaderParser parser; @Mock private AnimatedGifEncoder gifEncoder; @Mock private Resource frameResource; @Mock private GifDrawable gifDrawable; @Mock private Transformation frameTransformation; @Mock private Resource transformedResource; private ReEncodingGifResourceEncoder encoder; private Options options; private File file; @SuppressWarnings("unchecked") @Before public void setUp() { MockitoAnnotations.initMocks(this); Application context = ApplicationProvider.getApplicationContext(); ReEncodingGifResourceEncoder.Factory factory = mock(ReEncodingGifResourceEncoder.Factory.class); when(decoder.getNextFrame()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); when(factory.buildDecoder(any(GifDecoder.BitmapProvider.class))).thenReturn(decoder); when(factory.buildParser()).thenReturn(parser); when(factory.buildEncoder()).thenReturn(gifEncoder); when(factory.buildFrameResource(anyBitmapOrNull(), any(BitmapPool.class))) .thenReturn(frameResource); // TODO Util.anyResource once Util is moved to testutil module (remove unchecked above!) when(frameTransformation.transform(anyContext(), any(Resource.class), anyInt(), anyInt())) .thenReturn(frameResource); when(gifDrawable.getFrameTransformation()).thenReturn(frameTransformation); when(gifDrawable.getBuffer()).thenReturn(ByteBuffer.allocate(0)); when(resource.get()).thenReturn(gifDrawable); encoder = new ReEncodingGifResourceEncoder(context, mock(BitmapPool.class), factory); options = new Options(); options.set(ReEncodingGifResourceEncoder.ENCODE_TRANSFORMATION, true); file = new File(context.getCacheDir(), "test"); } @After public void tearDown() { // GC before delete() to release files on Windows (https://stackoverflow.com/a/4213208/253468) System.gc(); if (file.exists() && !file.delete()) { throw new RuntimeException("Failed to delete file"); } } @Test public void testEncodeStrategy_withEncodeTransformationTrue_returnsTransformed() { assertThat(encoder.getEncodeStrategy(options)).isEqualTo(EncodeStrategy.TRANSFORMED); } @Test public void testEncodeStrategy_withEncodeTransformationUnSet_returnsSource() { options.set(ReEncodingGifResourceEncoder.ENCODE_TRANSFORMATION, null); assertThat(encoder.getEncodeStrategy(options)).isEqualTo(EncodeStrategy.SOURCE); } @Test public void testEncodeStrategy_withEncodeTransformationFalse_returnsSource() { options.set(ReEncodingGifResourceEncoder.ENCODE_TRANSFORMATION, false); assertThat(encoder.getEncodeStrategy(options)).isEqualTo(EncodeStrategy.SOURCE); } @Test public void testEncode_withEncodeTransformationFalse_writesSourceDataToStream() throws IOException { options.set(ReEncodingGifResourceEncoder.ENCODE_TRANSFORMATION, false); String expected = "testString"; byte[] data = expected.getBytes("UTF-8"); when(gifDrawable.getBuffer()).thenReturn(ByteBuffer.wrap(data)); assertTrue(encoder.encode(resource, file, options)); assertThat(getEncodedData()).isEqualTo(expected); } @Test public void testEncode_WithEncodeTransformationFalse_whenOsThrows_returnsFalse() throws IOException { options.set(ReEncodingGifResourceEncoder.ENCODE_TRANSFORMATION, false); byte[] data = "testString".getBytes("UTF-8"); when(gifDrawable.getBuffer()).thenReturn(ByteBuffer.wrap(data)); assertThat(file.mkdirs()).isTrue(); assertFalse(encoder.encode(resource, file, options)); } @Test public void testReturnsFalseIfEncoderFailsToStart() { when(gifEncoder.start(any(OutputStream.class))).thenReturn(false); assertFalse(encoder.encode(resource, file, options)); } @Test public void testSetsDataOnParserBeforeParsingHeader() { ByteBuffer data = ByteBuffer.allocate(1); when(gifDrawable.getBuffer()).thenReturn(data); GifHeader header = mock(GifHeader.class); when(parser.parseHeader()).thenReturn(header); encoder.encode(resource, file, options); InOrder order = inOrder(parser, decoder); order.verify(parser).setData(eq(data)); order.verify(parser).parseHeader(); order.verify(decoder).setData(header, data); } @Test public void testAdvancesDecoderBeforeAttemptingToGetFirstFrame() { when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); when(decoder.getFrameCount()).thenReturn(1); when(decoder.getNextFrame()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); encoder.encode(resource, file, options); InOrder order = inOrder(decoder); order.verify(decoder).advance(); order.verify(decoder).getNextFrame(); } @Test public void testSetsDelayOnEncoderAfterAddingFrame() { when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); when(gifEncoder.addFrame(anyBitmapOrNull())).thenReturn(true); when(decoder.getFrameCount()).thenReturn(1); when(decoder.getNextFrame()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)); int expectedIndex = 34; when(decoder.getCurrentFrameIndex()).thenReturn(expectedIndex); int expectedDelay = 5000; when(decoder.getDelay(eq(expectedIndex))).thenReturn(expectedDelay); encoder.encode(resource, file, options); InOrder order = inOrder(gifEncoder, decoder); order.verify(decoder).advance(); order.verify(gifEncoder).addFrame(anyBitmapOrNull()); order.verify(gifEncoder).setDelay(eq(expectedDelay)); order.verify(decoder).advance(); } @Test public void testWritesSingleFrameToEncoderAndReturnsTrueIfEncoderFinishes() { Bitmap frame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(frameResource.get()).thenReturn(frame); when(decoder.getFrameCount()).thenReturn(1); when(decoder.getNextFrame()).thenReturn(frame); when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); when(gifEncoder.addFrame(eq(frame))).thenReturn(true); when(gifEncoder.finish()).thenReturn(true); assertTrue(encoder.encode(resource, file, options)); verify(gifEncoder).addFrame(eq(frame)); } @Test public void testReturnsFalseIfAddingFrameFails() { when(decoder.getFrameCount()).thenReturn(1); when(decoder.getNextFrame()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); when(gifEncoder.addFrame(anyBitmapOrNull())).thenReturn(false); assertFalse(encoder.encode(resource, file, options)); } @Test public void testReturnsFalseIfFinishingFails() { when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); when(gifEncoder.finish()).thenReturn(false); assertFalse(encoder.encode(resource, file, options)); } @Test public void testWritesTransformedBitmaps() { final Bitmap frame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(decoder.getFrameCount()).thenReturn(1); when(decoder.getNextFrame()).thenReturn(frame); when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); int expectedWidth = 123; int expectedHeight = 456; when(gifDrawable.getIntrinsicWidth()).thenReturn(expectedWidth); when(gifDrawable.getIntrinsicHeight()).thenReturn(expectedHeight); Bitmap transformedFrame = Bitmap.createBitmap(200, 200, Bitmap.Config.RGB_565); when(transformedResource.get()).thenReturn(transformedFrame); when(frameTransformation.transform( anyContext(), eq(frameResource), eq(expectedWidth), eq(expectedHeight))) .thenReturn(transformedResource); when(gifDrawable.getFrameTransformation()).thenReturn(frameTransformation); encoder.encode(resource, file, options); verify(gifEncoder).addFrame(eq(transformedFrame)); } @Test public void testRecyclesFrameResourceBeforeWritingIfTransformedResourceIsDifferent() { when(decoder.getFrameCount()).thenReturn(1); when(frameTransformation.transform(anyContext(), eq(frameResource), anyInt(), anyInt())) .thenReturn(transformedResource); Bitmap expected = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); when(transformedResource.get()).thenReturn(expected); when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); encoder.encode(resource, file, options); InOrder order = inOrder(frameResource, gifEncoder); order.verify(frameResource).recycle(); order.verify(gifEncoder).addFrame(eq(expected)); } @Test public void testRecyclesTransformedResourceAfterWritingIfTransformedResourceIsDifferent() { when(decoder.getFrameCount()).thenReturn(1); Bitmap expected = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565); when(transformedResource.get()).thenReturn(expected); when(frameTransformation.transform(anyContext(), eq(frameResource), anyInt(), anyInt())) .thenReturn(transformedResource); when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); encoder.encode(resource, file, options); InOrder order = inOrder(transformedResource, gifEncoder); order.verify(gifEncoder).addFrame(eq(expected)); order.verify(transformedResource).recycle(); } @Test public void testRecyclesFrameResourceAfterWritingIfFrameResourceIsNotTransformed() { when(decoder.getFrameCount()).thenReturn(1); when(frameTransformation.transform(anyContext(), eq(frameResource), anyInt(), anyInt())) .thenReturn(frameResource); Bitmap expected = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888); when(frameResource.get()).thenReturn(expected); when(gifEncoder.start(any(OutputStream.class))).thenReturn(true); encoder.encode(resource, file, options); InOrder order = inOrder(frameResource, gifEncoder); order.verify(gifEncoder).addFrame(eq(expected)); order.verify(frameResource).recycle(); } @Test public void testWritesBytesDirectlyToDiskIfTransformationIsUnitTransformation() { when(gifDrawable.getFrameTransformation()).thenReturn(UnitTransformation.get()); String expected = "expected"; when(gifDrawable.getBuffer()).thenReturn(ByteBuffer.wrap(expected.getBytes())); encoder.encode(resource, file, options); assertThat(getEncodedData()).isEqualTo(expected); verify(gifEncoder, never()).start(any(OutputStream.class)); verify(parser, never()).setData(any(byte[].class)); verify(parser, never()).parseHeader(); } private String getEncodedData() { try { return new String(ByteBufferUtil.toBytes(ByteBufferUtil.fromFile(file))); } catch (IOException e) { throw new RuntimeException(e); } } private static Context anyContext() { return any(Context.class); } private static Bitmap anyBitmapOrNull() { return any(); } } ================================================ FILE: integration/gradle.properties ================================================ # Prefix and postfix for source and javadoc jars. JAR_PREFIX=glide- JAR_POSTFIX=-integration ================================================ FILE: integration/ktx/api/ktx.api ================================================ public abstract interface annotation class com/bumptech/glide/integration/ktx/ExperimentGlideFlows : java/lang/annotation/Annotation { } public final class com/bumptech/glide/integration/ktx/FlowsKt { public static final fun flow (Lcom/bumptech/glide/RequestBuilder;)Lkotlinx/coroutines/flow/Flow; public static final fun flow (Lcom/bumptech/glide/RequestBuilder;I)Lkotlinx/coroutines/flow/Flow; public static final fun flow (Lcom/bumptech/glide/RequestBuilder;II)Lkotlinx/coroutines/flow/Flow; } public abstract class com/bumptech/glide/integration/ktx/GlideFlowInstant { public abstract fun getStatus ()Lcom/bumptech/glide/integration/ktx/Status; } public abstract interface annotation class com/bumptech/glide/integration/ktx/InternalGlideApi : java/lang/annotation/Annotation { } public final class com/bumptech/glide/integration/ktx/Placeholder : com/bumptech/glide/integration/ktx/GlideFlowInstant { public fun (Lcom/bumptech/glide/integration/ktx/Status;Landroid/graphics/drawable/Drawable;)V public final fun component1 ()Lcom/bumptech/glide/integration/ktx/Status; public final fun component2 ()Landroid/graphics/drawable/Drawable; public final fun copy (Lcom/bumptech/glide/integration/ktx/Status;Landroid/graphics/drawable/Drawable;)Lcom/bumptech/glide/integration/ktx/Placeholder; public static synthetic fun copy$default (Lcom/bumptech/glide/integration/ktx/Placeholder;Lcom/bumptech/glide/integration/ktx/Status;Landroid/graphics/drawable/Drawable;ILjava/lang/Object;)Lcom/bumptech/glide/integration/ktx/Placeholder; public fun equals (Ljava/lang/Object;)Z public final fun getPlaceholder ()Landroid/graphics/drawable/Drawable; public fun getStatus ()Lcom/bumptech/glide/integration/ktx/Status; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class com/bumptech/glide/integration/ktx/Resource : com/bumptech/glide/integration/ktx/GlideFlowInstant { public fun (Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;)V public final fun component1 ()Lcom/bumptech/glide/integration/ktx/Status; public final fun component2 ()Ljava/lang/Object; public final fun copy (Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;)Lcom/bumptech/glide/integration/ktx/Resource; public static synthetic fun copy$default (Lcom/bumptech/glide/integration/ktx/Resource;Lcom/bumptech/glide/integration/ktx/Status;Ljava/lang/Object;ILjava/lang/Object;)Lcom/bumptech/glide/integration/ktx/Resource; public fun equals (Ljava/lang/Object;)Z public final fun getResource ()Ljava/lang/Object; public fun getStatus ()Lcom/bumptech/glide/integration/ktx/Status; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class com/bumptech/glide/integration/ktx/Status : java/lang/Enum { public static final field CLEARED Lcom/bumptech/glide/integration/ktx/Status; public static final field FAILED Lcom/bumptech/glide/integration/ktx/Status; public static final field RUNNING Lcom/bumptech/glide/integration/ktx/Status; public static final field SUCCEEDED Lcom/bumptech/glide/integration/ktx/Status; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/bumptech/glide/integration/ktx/Status; public static fun values ()[Lcom/bumptech/glide/integration/ktx/Status; } ================================================ FILE: integration/ktx/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("com.android.library") id("kotlin-android") } android { namespace = "com.bumptech.glide.integration.ktx" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { getByName("release") { isMinifyEnabled = false } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } } // Enable strict mode, but exclude tests. tasks.withType(KotlinCompile::class.java).configureEach { if (!name.contains("Test")) { kotlinOptions.freeCompilerArgs += "-Xexplicit-api=strict" } } dependencies { api(project(":library")) implementation(libs.androidx.core.ktx) implementation(libs.coroutines.core) testImplementation(libs.androidx.espresso) testImplementation(libs.androidx.espresso.idling) testImplementation(libs.androidx.test.ktx) testImplementation(libs.kotlin.junit) testImplementation(libs.androidx.test.ktx.junit) testImplementation(libs.androidx.junit) testImplementation(libs.robolectric) testImplementation(libs.androidx.test.runner) testImplementation(libs.junit) testImplementation(libs.coroutines.test) testImplementation(libs.truth) androidTestImplementation(libs.androidx.junit) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/ktx/gradle.properties ================================================ POM_NAME=Glide Kotlin Extensions POM_ARTIFACT_ID=ktx POM_PACKAGING=aar POM_DESCRIPTION=An integration library to improve Kotlin interop with Glide VERSION_MAJOR=1 VERSION_MINOR=0 VERSION_PATCH=0 VERSION_NAME=1.0.0-beta08 ================================================ FILE: integration/ktx/src/main/java/com/bumptech/glide/GlideIntegration.kt ================================================ /** * Functions that give us access to some of Glide's non-public internals to make the flows API a bit * better. */ package com.bumptech.glide import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target internal fun RequestBuilder<*>.requestManager() = this.requestManager internal fun RequestBuilder.intoDirect( targetAndRequestListener: TargetAndRequestListenerT ) where TargetAndRequestListenerT : Target, TargetAndRequestListenerT : RequestListener { this.into(targetAndRequestListener, targetAndRequestListener) { it.run() } } ================================================ FILE: integration/ktx/src/main/java/com/bumptech/glide/integration/ktx/Flows.kt ================================================ package com.bumptech.glide.integration.ktx import android.graphics.drawable.Drawable import androidx.annotation.GuardedBy import com.bumptech.glide.RequestBuilder import com.bumptech.glide.intoDirect import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.Request import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.SizeReadyCallback import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.requestManager import com.bumptech.glide.util.Util import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "Glide's flow integration is very experimental and subject to breaking API or behavior changes", ) @Retention(AnnotationRetention.BINARY) @kotlin.annotation.Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) public annotation class ExperimentGlideFlows /** * The current status of a flow * * There is no well established graph that defines the valid Status transitions. Depending on * various factors like the parameters of the request, whether or not the resource is in the memory * cache, or even various calls to Glide's APIs, these may be emitted in different orders. As an * example, [RUNNING] is skipped if a request can be immediately completed from the memory cache. * * See [flow] for more details. */ @ExperimentGlideFlows public enum class Status { /** The load is not started or has been cleared. */ CLEARED, /** At least the primary load is still in progress. */ RUNNING, /** * The primary load or the error load ([RequestBuilder.error]) associated with the primary have * finished successfully. */ SUCCEEDED, /** The primary load has failed. One or more thumbnails may have succeeded. */ FAILED, } /** * Identical to [flow] with [Target.SIZE_ORIGINAL] as the dimensions * * This isn't generally a good idea, [Target.SIZE_ORIGINAL] is often much larger than you need. * Using it unnecessarily will waste memory and cache space. It will also slow down future loads * from the disk cache. * * Use this method only if you you expect the request and all of the subrequests ( * [RequestBuilder.override] and [RequestBuilder.error] to have specific sizes set). Validation is * only performed on the top level request because we cannot reliably verify all possible * subrequests. */ @ExperimentGlideFlows public fun RequestBuilder.flow(): Flow> { require(isValidOverride) { "At least your primary request is missing override dimensions. If you want to use" + " Target.SIZE_ORIGINAL, do so explicitly" } return flow(Target.SIZE_ORIGINAL) } /** Identical to `flow(dimension, dimension)` */ @ExperimentGlideFlows public fun RequestBuilder.flow( dimension: Int ): Flow> = flow(dimension, dimension) /** * Identical to [flow] with dimensions, except that the size is resolved asynchronously using * [waitForSize]. * * If an override size has been set using [RequestBuilder.override], that size will be used instead * and [waitForSize] may never be called. * * [Placeholder] values may be emitted prior to [waitForSize] returning. Similarly if * [RequestBuilder.thumbnail] requests are present and have overridden sizes, [Resource] values for * those thumbnails may also be emitted. [waitForSize] will only be used for requests where no * [RequestBuilder.override] size is available. * * If [waitForSize] does not return, this flow may never return values other than placeholders. * * This function is internal only, intended primarily for Compose. The Target API provides similar * functionality for traditional Views. We could consider expanding the visibility if there are use * cases for asynchronous size resolution outside of Glide's Compose integration. */ @InternalGlideApi @ExperimentGlideFlows public fun RequestBuilder.flow( waitForSize: suspend () -> Size ): Flow> = flow(AsyncGlideSize(waitForSize)) /** * Convert a load in Glide into a flow that emits placeholders and resources in the order they'd be * seen by a [Target]. * * Just like a [Target] there is no well defined end to a Glide request. Barring cancellation, the * flow should eventually reach [Status.SUCCEEDED] or [Status.FAILED] at least once. However * connectivity changes, calls to [com.bumptech.glide.RequestManager.pauseAllRequests] or * [com.bumptech.glide.RequestManager.resumeRequests], or the lifecycle associated with this request * may cause the request to be started multiple times. As long as the flow is active, callers will * receive emissions from every run. * * This flow will clear the associated Glide request when it's cancelled. This means that callers * must keep the flow active while any resource emitted by the flow is still in use. For UI * contexts, collecting the flow in the appropriate fragment or view model coroutine context is * sufficient as long as you avoid truncating methods like [kotlinx.coroutines.flow.take], * [kotlinx.coroutines.flow.takeWhile], etc. If you do use these methods, you must be sure that * you're no longer using or displaying the associated resource once the flow is no longer active * (ie [kotlinx.coroutines.flow.collect] finishes). One way to do this would be to mimic the UI by * creating and keeping active a coroutine context that collects from the flow while the resource is * in use. If this restriction is limiting for you, please file an issue on Github so we can think * of alternative options. * * If there have been any previous calls to this [RequestBuilder]'s * [com.bumptech.glide.request.RequestOptions.override] method, the size specified in that method * will be used instead of the size provided here. This includes calls where override sizes may have * been copied from other option sets via [RequestBuilder.apply]. */ @ExperimentGlideFlows @OptIn(InternalGlideApi::class) public fun RequestBuilder.flow( width: Int, height: Int, ): Flow> { require(Util.isValidDimensions(width, height)) return flow(Size(width = width, height = height)) } // We're not asserting on size here because it might come from RequestBuilder.override. Assertions // for provided sizes belong in those methods, assertions for overrides belong in the override // method. @InternalGlideApi @ExperimentGlideFlows private fun RequestBuilder.flow( size: Size ): Flow> = flowResolvable(ImmediateGlideSize(size)) @OptIn(ExperimentGlideFlows::class) @InternalGlideApi public fun RequestBuilder.flowResolvable( size: ResolvableGlideSize ): Flow> = flow(size) /** * A [Status] and value pair, where the value is either a [Placeholder] or a [Resource] depending on * how far the Glide load has progressed and/or how successful it's been. */ @ExperimentGlideFlows public sealed class GlideFlowInstant { public abstract val status: Status } /** * Wraps a [Status] and a placeholder [Drawable] (from [RequestBuilder.placeholder], * [RequestBuilder.fallback], [RequestBuilder.error] etc). */ @ExperimentGlideFlows public data class Placeholder( public override val status: Status, public val placeholder: Drawable?, ) : GlideFlowInstant() { init { require( when (status) { Status.SUCCEEDED -> false Status.CLEARED -> true // Placeholder will be present prior to the first thumbnail succeeding Status.RUNNING -> true Status.FAILED -> true } ) } } /** * Wraps a [Status] and a resource loaded from the primary request, a [RequestBuilder.thumbnail] * request, or a [RequestBuilder.error] request. * * **Status.FAILED** is a perfectly valid status with this class. If the primary request fails, but * at least one thumbnail succeeds, the flow will emit `Resource(FAILED, resource)` to indicate both * that we have some value but also that the primary request has failed. */ @ExperimentGlideFlows public data class Resource( public override val status: Status, public val resource: ResourceT, ) : GlideFlowInstant() { init { require( when (status) { Status.SUCCEEDED -> true // A load with thumbnail(s) where the thumbnail(s) have finished but not the main // request Status.RUNNING -> true // The primary request of the load failed, but at least one thumbnail was // successful. Status.FAILED -> true // Once the load is cleared, it can only show a placeholder Status.CLEARED -> false } ) } } @InternalGlideApi @ExperimentGlideFlows private fun RequestBuilder.flow( size: ResolvableGlideSize ): Flow> { val requestBuilder = this val requestManager = requestBuilder.requestManager() return callbackFlow { val target = FlowTarget(this, size) requestBuilder.intoDirect(target) awaitClose { requestManager.clear(target) } } } /** * Observes a glide request using [Target] and [RequestListener] and tries to emit something * resembling a coherent set of placeholders and resources for it. * * Threading in this class is a bit complicated. As a general rule, the callback methods are ordered * by callers. So we have to handle being called from multiple threads, but we don't need to try to * handle callbacks being called in parallel. * * The primary area of concern around thread is that [resolvedSize] and [sizeReadyCallbacks] must be * updated atomically, but can be modified on different threads. * * [currentRequest] would normally be a concern because [Target]s can be cancelled on threads other * than where they were started. However in our case, [currentRequest] is set once when our request * is started (by us) and is only cancelled when the request finishes. So we just have to avoid NPEs * and make sure the state is reasonably up to date. * * [lastResource] is an unfortunate hack that tries to make sure that we emit [Status.FAILED] if a * thumbnail request succeeds, but then the primary request fails. In that case, we'd normally * already have emitted [Resource] with [Status.RUNNING] and the thumbnail value and then we'd emit * nothing else. That's not very satisfying for callers who expect some resolution. So instead we * track the last resource produced by thumbnails and emit that along with [Status.FAILED] when we * see that the primary request has failed. As a result we're not concerned with ordering with * regards to [lastResource], but it is possible the callbacks will be called on different threads, * so the value may be updated from different threads even if it's not concurrent. */ @ExperimentGlideFlows @InternalGlideApi private class FlowTarget( private val scope: ProducerScope>, private val size: ResolvableGlideSize, ) : Target, RequestListener { @Volatile private var resolvedSize: Size? = null @Volatile private var currentRequest: Request? = null @Volatile private var lastResource: ResourceT? = null @GuardedBy("this") private val sizeReadyCallbacks = mutableListOf() init { when (size) { // If we have a size, skip the coroutine, we can continue immediately. is ImmediateGlideSize -> resolvedSize = size.size // Otherwise, we do not want to block the flow while waiting on a size because one or // more // requests in the chain may have a fixed size, even if the primary request does not. // Starting the Glide request right away allows any subrequest that has a fixed size to // begin immediately, shaving off some small amount of time. is AsyncGlideSize -> scope.launch { val localResolvedSize = size.asyncSize() val callbacksToNotify: List synchronized(this) { resolvedSize = localResolvedSize callbacksToNotify = ArrayList(sizeReadyCallbacks) sizeReadyCallbacks.clear() } callbacksToNotify.forEach { it.onSizeReady(localResolvedSize.width, localResolvedSize.height) } } } } override fun onStart() {} override fun onStop() {} override fun onDestroy() {} override fun onLoadStarted(placeholder: Drawable?) { lastResource = null scope.trySend(Placeholder(Status.RUNNING, placeholder)) } override fun onLoadFailed(errorDrawable: Drawable?) { scope.trySend(Placeholder(Status.FAILED, errorDrawable)) } override fun onResourceReady(resource: ResourceT, transition: Transition?) { lastResource = resource scope.trySend( Resource( // currentRequest is the entire request state, so we can use it to figure out if // this // resource is from a thumbnail request (isComplete is false) or the primary // request. if (currentRequest?.isComplete == true) Status.SUCCEEDED else Status.RUNNING, resource, ) ) } override fun onLoadCleared(placeholder: Drawable?) { lastResource = null scope.trySend(Placeholder(Status.CLEARED, placeholder)) } override fun getSize(cb: SizeReadyCallback) { val localResolvedSize = resolvedSize if (localResolvedSize != null) { cb.onSizeReady(localResolvedSize.width, localResolvedSize.height) return } synchronized(this@FlowTarget) { val lockedResolvedSize = resolvedSize if (lockedResolvedSize != null) { cb.onSizeReady(lockedResolvedSize.width, lockedResolvedSize.height) } else { sizeReadyCallbacks.add(cb) } } } override fun removeCallback(cb: SizeReadyCallback) { synchronized(this) { sizeReadyCallbacks.remove(cb) } } override fun setRequest(request: Request?) { currentRequest = request } override fun getRequest(): Request? { return currentRequest } override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean, ): Boolean { val localLastResource = lastResource val localRequest = currentRequest if ( localLastResource != null && localRequest?.isComplete == false && !localRequest.isRunning ) { scope.channel.trySend(Resource(Status.FAILED, localLastResource)) } return false } override fun onResourceReady( resource: ResourceT, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean, ): Boolean { return false } } @InternalGlideApi public data class Size(val width: Int, val height: Int) { init { require(width.isValidGlideDimension()) require(height.isValidGlideDimension()) } } @InternalGlideApi public sealed class ResolvableGlideSize @InternalGlideApi public data class ImmediateGlideSize(val size: Size) : ResolvableGlideSize() @InternalGlideApi public data class AsyncGlideSize(val asyncSize: suspend () -> Size) : ResolvableGlideSize() @InternalGlideApi public fun Int.isValidGlideDimension(): Boolean = Util.isValidDimension(this) ================================================ FILE: integration/ktx/src/main/java/com/bumptech/glide/integration/ktx/InternalGlideApi.kt ================================================ package com.bumptech.glide.integration.ktx @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "An internal only API not intended for public use, may change, break or be removed" + " at any time without warning.", ) @Retention(AnnotationRetention.BINARY) @kotlin.annotation.Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) public annotation class InternalGlideApi ================================================ FILE: integration/ktx/src/test/java/com/bumptech/glide/integration/ktx/FlowsTest.kt ================================================ @file:OptIn(InternalGlideApi::class, ExperimentGlideFlows::class, ExperimentalCoroutinesApi::class) package com.bumptech.glide.integration.ktx import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onIdle import androidx.test.ext.junit.runners.AndroidJUnit4 import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import com.bumptech.glide.RequestManager import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.Key import com.bumptech.glide.load.Options import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.engine.cache.MemoryCache import com.bumptech.glide.load.engine.executor.GlideExecutor import com.bumptech.glide.load.engine.executor.GlideIdlingResources import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.google.common.truth.Correspondence import com.google.common.truth.IterableSubject import com.google.common.truth.Truth.assertThat import java.io.File import java.lang.RuntimeException import java.util.concurrent.atomic.AtomicReference import kotlin.reflect.KClass import kotlin.test.assertFailsWith import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith // newFile throws IOException, which triggers this warning even though there's no reasonable // alternative :/. @Suppress("BlockingMethodInNonBlockingContext", "RedundantSuppression") @RunWith(AndroidJUnit4::class) class FlowsTest { private val context = ApplicationProvider.getApplicationContext() @get:Rule val temporaryFolder = TemporaryFolder() @Before fun setUp() { GlideIdlingResources.initGlide() } @After fun tearDown() { Glide.tearDown() } @Test fun flow_withPlaceholderDrawable_emitsPlaceholderDrawableFirst() = runTest { val placeholderDrawable = ColorDrawable(Color.RED) val first = Glide.with(context) .load(temporaryFolder.newFile()) .placeholder(placeholderDrawable) .flow(100) .first() assertThat(first).isEqualTo(Placeholder(Status.RUNNING, placeholderDrawable)) } @Test fun flow_withNoPlaceholderDrawable_emitsNullPlaceholderFirst() = runTest { val first = Glide.with(context).load(temporaryFolder.newFile()).flow(100).first() assertThat(first).isEqualTo(Placeholder(Status.RUNNING, placeholder = null)) } @Test fun flow_failingNonNullModel_emitsRunningThenFailed() = runTest { val missingResourceId = 123 val results = Glide.with(context).load(missingResourceId).flow(100).firstLoad().toList() assertThat(results) .containsExactly( Placeholder(Status.RUNNING, placeholder = null), Placeholder(Status.FAILED, placeholder = null), ) .inOrder() } @Test fun flow_failingNonNullModel_whenRestartedAfterFailure_emitsSecondLoad() = runTest { val requestManager = Glide.with(context) val missingResourceId = 123 val flow = requestManager .load(missingResourceId) .listener(onFailure(atMostOnce { restartAllRequestsOnNewThread(requestManager) })) .flow(100) assertThat(flow.take(4).toList()) .comparingStatus() .containsExactly(Status.RUNNING, Status.FAILED, Status.RUNNING, Status.FAILED) } @Test fun flow_successfulNonNullModel_emitsRunningThenSuccess() = runTest { val results = Glide.with(context).load(newImageFile()).flow(100).firstLoad().toList() assertThat(results) .compareStatusAndType() .containsExactly(placeholder(Status.RUNNING), resource(Status.SUCCEEDED)) .inOrder() } @Test fun flow_withNullModel_andFallbackDrawable_emitsFailureWithFallbackDrawable() = runTest { val fallbackDrawable = ColorDrawable(Color.BLUE) val first = Glide.with(context).load(null as Uri?).fallback(fallbackDrawable).flow(100).first() assertThat(first).isEqualTo(Placeholder(Status.FAILED, fallbackDrawable)) } @Test fun flow_successfulNonNullModel_whenRestartedAfterSuccess_emitsSecondLoad() = runTest { val requestManager = Glide.with(context) val flow = requestManager .load(newImageFile()) .listener(onSuccess(atMostOnce { restartAllRequestsOnNewThread(requestManager) })) .flow(100) assertThat(flow.take(4).toList()) .comparingStatus() .containsExactly( Status.RUNNING, Status.SUCCEEDED, Status.CLEARED, // See the TODO in RequestTracker#pauseAllRequests Status .SUCCEEDED, // The request completes from in memory, so it never goes to RUNNING ) } @Test fun flow_successfulNonNullModel_oneSuccessfulThumbnail_emitsThumbnailAndMainResources() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val output = Glide.with(context) .load(newImageFile()) .thumbnail(Glide.with(context).load(newImageFile())) .flow(100) .firstLoad() .toList() assertThat(output) .compareStatusAndType() .containsExactly( placeholder(Status.RUNNING), resource(Status.RUNNING), resource(Status.SUCCEEDED), ) } @Test fun flow_successfulNonNullModel_oneFailingThumbnail_emitMainResourceOnly() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val missingResourceId = 123 val output = Glide.with(context) .load(newImageFile()) .thumbnail(Glide.with(context).load(missingResourceId)) .flow(100) .firstLoad() .toList() assertThat(output) .compareStatusAndType() .containsExactly(placeholder(Status.RUNNING), resource(Status.SUCCEEDED)) } @Test fun flow_failingNonNullModel_successfulThumbnail_emitsThumbnailWithFailedStatus() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val missingResourceId = 123 val output = Glide.with(context) .load(missingResourceId) .thumbnail(Glide.with(context).load(newImageFile())) .flow(100) .firstLoad() .toList() assertThat(output) .compareStatusAndType() .containsExactly( placeholder(Status.RUNNING), resource(Status.RUNNING), resource(Status.FAILED), ) } @Test fun flow_failingNonNullModel_failingNonNullThumbnail_emitsRunningThenFailed() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val missingResourceId = 123 val output = Glide.with(context) .load(missingResourceId) .thumbnail(Glide.with(context).load(missingResourceId)) .flow(100) .firstLoad() .toList() assertThat(output) .compareStatusAndType() .containsExactly(placeholder(Status.RUNNING), placeholder(Status.FAILED)) } @Test fun flow_failingNonNullModel_succeedingNonNullError_emitsRunningThenSuccess() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val missingResourceId = 123 val output = Glide.with(context) .load(missingResourceId) .error(Glide.with(context).load(newImageFile())) .flow(100) .firstLoad() .toList() assertThat(output) .compareStatusAndType() .containsExactly( placeholder(Status.RUNNING), // TODO(judds): This is probably another case where resource(Status.FAILURE) is more // appropriate. TO do so, we'd need to avoid passing TargetListener in // RequestBuilder into // thumbnails (and probably error request builders). That's a larger change resource(Status.SUCCEEDED), ) } @Test fun flow_failingNonNullModel_failingNonNullError_emitsRunningThenFailure() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val missingResourceId = 123 val output = Glide.with(context) .load(missingResourceId) .error(Glide.with(context).load(missingResourceId)) .flow(100) .firstLoad() .toList() assertThat(output) .compareStatusAndType() .containsExactly(placeholder(Status.RUNNING), placeholder(Status.FAILED)) } @Test fun flow_failingNonNullModel_failingNonNullError_succeedingErrorThumbnail_emitsRunningThenRunningWithResourceThenFailureWithResource() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val missingResourceId = 123 val output = Glide.with(context) .load(missingResourceId) .error( Glide.with(context) .load(missingResourceId) .thumbnail(Glide.with(context).load(newImageFile())) ) .flow(100) .firstLoad() .toList() assertThat(output) .compareStatusAndType() .containsExactly( placeholder(Status.RUNNING), resource(Status.RUNNING), resource(Status.FAILED), ) } @Test fun flow_onClose_clearsTarget() = runTest { val inCache = AtomicReference?>() GlideIdlingResources.initGlide( GlideBuilder() .setMemoryCache( object : MemoryCache { override fun getCurrentSize(): Long = 0 override fun getMaxSize(): Long = 0 override fun setSizeMultiplier(multiplier: Float) {} override fun remove(key: Key): com.bumptech.glide.load.engine.Resource<*>? { return null } override fun setResourceRemovedListener( listener: MemoryCache.ResourceRemovedListener ) {} override fun clearMemory() {} override fun trimMemory(level: Int) {} override fun put( key: Key, resource: com.bumptech.glide.load.engine.Resource<*>?, ): com.bumptech.glide.load.engine.Resource<*>? { inCache.set(resource) return null } } ) ) val data = Glide.with(context).load(newImageFile()).flow(100, 100).firstLoad().toList() assertThat(data).isNotEmpty() // Glide's executor (in EngineJob's notify loop) and the coroutine race to close the // resource. // If Glide's executor wins, then the coroutine will be able to put the resource in the // cache, // but if not, the item won't be cached until after the coroutine starts back up. onIdle() assertThat(inCache.get()).isNotNull() } @Test fun flow_withOverrideSize_andProvidedSize_prefersOverrideSize() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context).load(FakeModel()).override(50, 60).flow(200, 100).firstLoad().toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(50, 60)) } @Test fun flow_withOnlyProvidedSize_usesProvidedSize() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context).load(FakeModel()).flow(100, 200).firstLoad().toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(100, 200)) } @Test fun flow_withOnlySingleDimension_usesProvidedSize() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context).load(FakeModel()).flow(150).firstLoad().toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(150, 150)) } @Test fun flow_withSizeOriginal_usesSizeOriginal() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context) .load(FakeModel()) .flow(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .firstLoad() .toList() assertThat(requestedSizeReference.get()) .isEqualTo(Size(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)) } @Test fun flow_withSizeOriginalOverride_concreteProvidedSize_usesSizeOriginal() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context) .load(FakeModel()) .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .flow(200, 300) .firstLoad() .toList() assertThat(requestedSizeReference.get()) .isEqualTo(Size(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)) } @Test fun flow_withConcreteOverride_sizeOriginalProvidedSize_usesConcreteSize() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context) .load(FakeModel()) .override(200, 300) .flow(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .firstLoad() .toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(200, 300)) } @Test fun flow_withThumbnailWithOverrideSize_usesOverrideSizeForThumbnail() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context) .load(newImageFile()) .thumbnail(Glide.with(context).load(FakeModel()).override(100, 200)) .flow(Target.SIZE_ORIGINAL) .firstLoad() .toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(100, 200)) } @Test fun flow_withThumbnailWithoutOverrideSize_usesProvidedSizeForThumbnail() = runTest { makeGlideSingleThreadedToOrderThumbnailRequests() val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context) .load(newImageFile()) .thumbnail(Glide.with(context).load(FakeModel())) .flow(300, 400) .firstLoad() .toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(300, 400)) } @Test fun flow_withInvalidProvidedWith_throws() = runTest { val missingResourceId = 123 val requestBuilder = Glide.with(context).load(missingResourceId) assertFailsWith { requestBuilder.flow(-100, 100) } } @Test fun flow_withInvalidProvidedHeight_throws() { val missingResourceId = 123 val requestBuilder = Glide.with(context).load(missingResourceId) assertFailsWith { requestBuilder.flow(100, -100) } } @Test fun flow_withAsyncSize_immediatelyEmitsPlaceholder() = runTest { val placeholder = ColorDrawable(Color.GREEN) val missingResourceId = 123 val result = Glide.with(context) .load(missingResourceId) .placeholder(placeholder) .flow(delayForever) .first() assertThat(result).isEqualTo(Placeholder(Status.RUNNING, placeholder)) } @Test fun flow_withAsyncSizeThatNeverCompletes_andOverrideSize_finishesSuccessfully() = runTest { val result = Glide.with(context) .load(newImageFile()) .override(100, 100) .flow(delayForever) .firstLoad() .toList() assertThat(result) .comparingStatus() .containsExactly(Status.RUNNING, Status.SUCCEEDED) .inOrder() } @Test fun flow_withAsyncSize_andOverrideSize_usesOverrideSize() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context) .load(FakeModel()) .override(200, 100) .flow { Size(1, 2) } .firstLoad() .toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(200, 100)) } @Test fun flow_withAsyncSize_thumbnailWithConcreteSize_startsThumbnailWithoutWaitingForSize() = runTest { val result = Glide.with(context) .load(newImageFile()) .thumbnail(Glide.with(context).load(newImageFile()).override(25, 50)) .flow(delayForever) .take(2) .toList() assertThat(result) .compareStatusAndType() .containsExactly(placeholder(Status.RUNNING), resource(Status.RUNNING)) .inOrder() } @Test fun flow_withAsyncSize_concreteSizeForThumbnail_startsMainRequestWhenAsyncSizeIsAvailable() = runTest { val waitForThumbnailToFinishChannel = Channel() val waitForThumbnailToFinishSize: suspend () -> Size = { waitForThumbnailToFinishChannel.receive() Size(100, 200) } val result = Glide.with(context) .load(newImageFile()) .thumbnail( Glide.with(context) .load(newImageFile()) .override(75, 50) .listener( onSuccess { launch { waitForThumbnailToFinishChannel.send(true) } } ) ) .flow(waitForThumbnailToFinishSize) .firstLoad() .toList() assertThat(result) .compareStatusAndType() .containsExactly( placeholder(Status.RUNNING), resource(Status.RUNNING), resource(Status.SUCCEEDED), ) } // TODO(judds): Consider adding a test for invalid async sizes. It doesn't seem like Glide // asserts on this in the existing framework, so it's probably not super important to do for // flows, but it might be nice. @Test fun flow_withNoProvidedSize_overrideSizePresent_usesOverrideSize() = runTest { val requestedSizeReference = registerSizeCapturingFakeModelLoader() Glide.with(context).load(FakeModel()).override(4, 5).flow().firstLoad().toList() assertThat(requestedSizeReference.get()).isEqualTo(Size(4, 5)) } @Test fun flow_withNoProvidedSize_overrideSizeMissing_throws() = runTest { val requestBuilder = Glide.with(context).load(FakeModel()) assertFailsWith { requestBuilder.flow() } } private val delayForever: suspend () -> Size = { delay(kotlin.time.Duration.INFINITE) throw RuntimeException() } private fun registerSizeCapturingFakeModelLoader(): AtomicReference { val result = AtomicReference() Glide.get(context) .registry .append( FakeModel::class.java, File::class.java, SizeObservingFakeModelLoader.Factory(newImageFile(), result), ) return result } // Avoid race conditions where the main request finishes first by making sure they execute // sequentially using a single threaded executor. private fun makeGlideSingleThreadedToOrderThumbnailRequests() { Glide.init( context, GlideBuilder() .setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build()), ) } // Robolectric will produce a Bitmap from any File, but this is relatively easy and will work on // emulators as well as robolectric. private fun newImageFile(): File { val file = temporaryFolder.newFile() val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) canvas.drawColor(Color.GREEN) file.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 75, it) } return file } class FakeModel class SizeObservingFakeModelLoader( private val fileLoader: ModelLoader, private val fakeResult: File, private val sizeReference: AtomicReference, ) : ModelLoader { override fun buildLoadData( model: FakeModel, width: Int, height: Int, options: Options, ): ModelLoader.LoadData? { sizeReference.set(Size(width, height)) return fileLoader.buildLoadData(fakeResult, width, height, options) } override fun handles(model: FakeModel): Boolean = true class Factory( private val fakeResult: File, private val sizeReference: AtomicReference, ) : ModelLoaderFactory { override fun build( multiFactory: MultiModelLoaderFactory ): ModelLoader { return SizeObservingFakeModelLoader( multiFactory.build(File::class.java, File::class.java), fakeResult, sizeReference, ) } override fun teardown() {} } } } private fun atMostOnce(function: () -> Unit): () -> Unit { var isCalled = false return { if (!isCalled) { isCalled = true function() } } } private fun onSuccess(onSuccess: () -> Unit) = simpleRequestListener(onSuccess) {} private fun onFailure(onFailure: () -> Unit) = simpleRequestListener({}, onFailure) private fun simpleRequestListener( onSuccess: () -> Unit, onFailure: () -> Unit, ): RequestListener = object : RequestListener { override fun onResourceReady( resource: ResourceT?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean, ): Boolean { onSuccess() return false } override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean, ): Boolean { onFailure() return false } } // TODO(judds): This function may be useful in production code as well, consider exposing it. private fun Flow>.firstLoad(): Flow> { val originalFlow = this return flow { var completion: GlideFlowInstant? = null originalFlow .takeWhile { if (it.status != Status.SUCCEEDED && it.status != Status.FAILED) { true } else { completion = it false } } .collect { emit(it) } emit(completion!!) } } @OptIn(DelicateCoroutinesApi::class) private fun restartAllRequestsOnNewThread(requestManager: RequestManager) = newSingleThreadContext("restart").use { it.executor.execute { requestManager.pauseAllRequests() requestManager.resumeRequests() } } private fun placeholder(status: Status) = StatusAndType(status, Placeholder::class) private fun resource(status: Status) = StatusAndType(status, Resource::class) private data class StatusAndType(val status: Status, val type: KClass>) private fun IterableSubject.compareStatusAndType() = comparingElementsUsing(statusAndType()) private fun statusAndType(): Correspondence, StatusAndType> = ktCorrespondenceFrom("statusAndType") { actual, expected -> actual?.statusAndType() == expected } private fun GlideFlowInstant<*>.statusAndType() = StatusAndType( status, when (this) { is Placeholder<*> -> Placeholder::class is Resource<*> -> Resource::class }, ) private fun IterableSubject.comparingStatus() = comparingElementsUsing(status()) private fun status(): Correspondence, Status> = ktCorrespondenceFrom("status") { actual, expected -> actual?.status == expected } private fun ktCorrespondenceFrom( description: String, predicate: Correspondence.BinaryPredicate, ) = Correspondence.from(predicate, description) ================================================ FILE: integration/ktx/src/test/java/com/bumptech/glide/load/engine/executor/GlideIdlingResourceInit.kt ================================================ package com.bumptech.glide.load.engine.executor import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.idling.concurrent.IdlingThreadPoolExecutor import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit object GlideIdlingResources { fun initGlide(builder: GlideBuilder? = null) { val registry = IdlingRegistry.getInstance() val executor = IdlingThreadPoolExecutor( "glide_test_thread", /* corePoolSize = */ 1, /* maximumPoolSize = */ 1, /* keepAliveTime = */ 1, TimeUnit.SECONDS, LinkedBlockingQueue(), ) { Thread(it) } val glideExecutor = GlideExecutor(executor) Glide.init( ApplicationProvider.getApplicationContext(), (builder ?: GlideBuilder()) .setSourceExecutor(glideExecutor) .setAnimationExecutor(glideExecutor) .setDiskCacheExecutor(glideExecutor), ) registry.register(executor) } } ================================================ FILE: integration/okhttp/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.okhttp" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) annotationProcessor(project(":annotation:compiler")) api(libs.okhttp2) api(libs.androidx.annotation) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/okhttp/gradle.properties ================================================ POM_NAME=Glide OkHttp Integration POM_ARTIFACT_ID=okhttp-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library to use OkHttp 2.x to fetch data over http/https in Glide ================================================ FILE: integration/okhttp/lint.xml ================================================ ================================================ FILE: integration/okhttp/src/main/java/com/bumptech/glide/integration/okhttp/OkHttpGlideModule.java ================================================ package com.bumptech.glide.integration.okhttp; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import com.bumptech.glide.load.model.GlideUrl; import java.io.InputStream; /** * A {@link com.bumptech.glide.module.GlideModule} implementation to replace Glide's default {@link * java.net.HttpURLConnection} based {@link com.bumptech.glide.load.model.ModelLoader} with an * OkHttp based {@link com.bumptech.glide.load.model.ModelLoader}. * *

    If you're using gradle, you can include this module simply by depending on the aar, the module * will be merged in by manifest merger. For other build systems or for more more information, see * {@link com.bumptech.glide.module.GlideModule}. * * @deprecated replaced with com.bumptech.glide.integration.okhttp3.OkHttpGlideModule. */ @Deprecated public class OkHttpGlideModule implements com.bumptech.glide.module.GlideModule { @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { // Do nothing. } @Override public void registerComponents(Context context, Glide glide, Registry registry) { registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); } } ================================================ FILE: integration/okhttp/src/main/java/com/bumptech/glide/integration/okhttp/OkHttpLibraryGlideModule.java ================================================ package com.bumptech.glide.integration.okhttp; import android.content.Context; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.LibraryGlideModule; import java.io.InputStream; /** * Registers OkHttp related classes via Glide's annotation processor. * *

    For Applications that depend on this library and include an {@link AppGlideModule} and Glide's * annotation processor, this class will be automatically included. * * @deprecated Prefer the okhttp3 version instead. */ @GlideModule @Deprecated public class OkHttpLibraryGlideModule extends LibraryGlideModule { @Override public void registerComponents(Context context, Glide glide, Registry registry) { registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); } } ================================================ FILE: integration/okhttp/src/main/java/com/bumptech/glide/integration/okhttp/OkHttpStreamFetcher.java ================================================ package com.bumptech.glide.integration.okhttp; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.HttpException; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.util.ContentLengthInputStream; import com.bumptech.glide.util.Synthetic; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; import java.io.IOException; import java.io.InputStream; import java.util.Map; /** * Fetches an {@link InputStream} using the okhttp library. * * @deprecated replaced with com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher. */ @Deprecated public class OkHttpStreamFetcher implements DataFetcher { private static final String TAG = "OkHttpFetcher"; private final OkHttpClient client; private final GlideUrl url; @SuppressWarnings("WeakerAccess") @Synthetic InputStream stream; @SuppressWarnings("WeakerAccess") @Synthetic ResponseBody responseBody; // Public API. @SuppressWarnings("WeakerAccess") public OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) { this.client = client; this.url = url; } @Override public void loadData( @NonNull Priority priority, @NonNull final DataCallback callback) { Request.Builder requestBuilder = new Request.Builder().url(url.toStringUrl()); for (Map.Entry headerEntry : url.getHeaders().entrySet()) { String key = headerEntry.getKey(); requestBuilder.addHeader(key, headerEntry.getValue()); } Request request = requestBuilder.build(); client .newCall(request) .enqueue( new com.squareup.okhttp.Callback() { @Override public void onFailure(Request request, IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "OkHttp failed to obtain result", e); } callback.onLoadFailed(e); } @Override public void onResponse(Response response) throws IOException { responseBody = response.body(); if (response.isSuccessful()) { long contentLength = responseBody.contentLength(); stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength); callback.onDataReady(stream); } else { callback.onLoadFailed(new HttpException(response.message(), response.code())); } } }); } @Override public void cleanup() { try { if (stream != null) { stream.close(); } } catch (IOException e) { // Ignored } if (responseBody != null) { try { responseBody.close(); } catch (IOException e) { // Ignored. } } } @Override public void cancel() { // TODO: call cancel on the client when this method is called on a background thread. See #257 } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.REMOTE; } } ================================================ FILE: integration/okhttp/src/main/java/com/bumptech/glide/integration/okhttp/OkHttpUrlLoader.java ================================================ package com.bumptech.glide.integration.okhttp; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.squareup.okhttp.OkHttpClient; import java.io.InputStream; /** * A simple model loader for fetching media over http/https using OkHttp. * * @deprecated replaced with com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader. */ @Deprecated public class OkHttpUrlLoader implements ModelLoader { private final OkHttpClient client; // Public API. @SuppressWarnings("WeakerAccess") public OkHttpUrlLoader(OkHttpClient client) { this.client = client; } @Override public boolean handles(@NonNull GlideUrl url) { return true; } @SuppressWarnings("deprecation") @Override public LoadData buildLoadData( @NonNull GlideUrl model, int width, int height, @NonNull Options options) { return new LoadData<>(model, new OkHttpStreamFetcher(client, model)); } /** The default factory for {@link OkHttpUrlLoader}s. */ // Public API. @SuppressWarnings({"WeakerAccess", "deprecation"}) public static class Factory implements ModelLoaderFactory { private static volatile OkHttpClient internalClient; private final OkHttpClient client; private static OkHttpClient getInternalClient() { if (internalClient == null) { synchronized (Factory.class) { if (internalClient == null) { internalClient = new OkHttpClient(); } } } return internalClient; } /** Constructor for a new Factory that runs requests using a static singleton client. */ public Factory() { this(getInternalClient()); } /** Constructor for a new Factory that runs requests using given client. */ public Factory(OkHttpClient client) { this.client = client; } @NonNull @SuppressWarnings("deprecation") @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new OkHttpUrlLoader(client); } @Override public void teardown() { // Do nothing, this instance doesn't own the client. } } } ================================================ FILE: integration/okhttp3/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.okhttp" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) annotationProcessor(project(":annotation:compiler")) api(libs.okhttp3) api(libs.androidx.annotation) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/okhttp3/gradle.properties ================================================ POM_NAME=Glide OkHttp 3.x Integration POM_ARTIFACT_ID=okhttp3-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library to use OkHttp 3.x to fetch data over http/https in Glide ================================================ FILE: integration/okhttp3/lint.xml ================================================ ================================================ FILE: integration/okhttp3/src/main/AndroidManifest.xml ================================================ ================================================ FILE: integration/okhttp3/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpGlideModule.java ================================================ package com.bumptech.glide.integration.okhttp3; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import com.bumptech.glide.load.model.GlideUrl; import java.io.InputStream; /** * A {@link com.bumptech.glide.module.GlideModule} implementation to replace Glide's default {@link * java.net.HttpURLConnection} based {@link com.bumptech.glide.load.model.ModelLoader} with an * OkHttp based {@link com.bumptech.glide.load.model.ModelLoader}. * *

    If you're using gradle, you can include this module simply by depending on the aar, the module * will be merged in by manifest merger. For other build systems or for more more information, see * {@link com.bumptech.glide.module.GlideModule}. * * @deprecated Replaced by {@link OkHttpLibraryGlideModule} for Applications that use Glide's * annotations. */ @Deprecated public class OkHttpGlideModule implements com.bumptech.glide.module.GlideModule { @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { // Do nothing. } @Override public void registerComponents(Context context, Glide glide, Registry registry) { registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); } } ================================================ FILE: integration/okhttp3/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpLibraryGlideModule.java ================================================ package com.bumptech.glide.integration.okhttp3; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.LibraryGlideModule; import java.io.InputStream; /** * Registers OkHttp related classes via Glide's annotation processor. * *

    For Applications that depend on this library and include an {@link AppGlideModule} and Glide's * annotation processor, this class will be automatically included. */ @GlideModule public final class OkHttpLibraryGlideModule extends LibraryGlideModule { @Override public void registerComponents( @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); } } ================================================ FILE: integration/okhttp3/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpStreamFetcher.java ================================================ package com.bumptech.glide.integration.okhttp3; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.HttpException; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.util.ContentLengthInputStream; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import java.io.InputStream; import java.util.Map; import okhttp3.Call; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; /** Fetches an {@link InputStream} using the okhttp library. */ public class OkHttpStreamFetcher implements DataFetcher, okhttp3.Callback { private static final String TAG = "OkHttpFetcher"; private final Call.Factory client; private final GlideUrl url; private InputStream stream; private ResponseBody responseBody; private DataCallback callback; // call may be accessed on the main thread while the object is in use on other threads. All other // accesses to variables may occur on different threads, but only one at a time. private volatile Call call; // Public API. @SuppressWarnings("WeakerAccess") public OkHttpStreamFetcher(Call.Factory client, GlideUrl url) { this.client = client; this.url = url; } @Override public void loadData( @NonNull Priority priority, @NonNull final DataCallback callback) { Request.Builder requestBuilder = new Request.Builder().url(url.toStringUrl()); for (Map.Entry headerEntry : url.getHeaders().entrySet()) { String key = headerEntry.getKey(); requestBuilder.addHeader(key, headerEntry.getValue()); } Request request = requestBuilder.build(); this.callback = callback; call = client.newCall(request); call.enqueue(this); } @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "OkHttp failed to obtain result", e); } callback.onLoadFailed(e); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) { responseBody = response.body(); if (response.isSuccessful()) { long contentLength = Preconditions.checkNotNull(responseBody).contentLength(); stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength); callback.onDataReady(stream); } else { callback.onLoadFailed(new HttpException(response.message(), response.code())); } } @Override public void cleanup() { try { if (stream != null) { stream.close(); } } catch (IOException e) { // Ignored } if (responseBody != null) { responseBody.close(); } callback = null; } @Override public void cancel() { Call local = call; if (local != null) { local.cancel(); } } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.REMOTE; } } ================================================ FILE: integration/okhttp3/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpUrlLoader.java ================================================ package com.bumptech.glide.integration.okhttp3; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import java.io.InputStream; import okhttp3.Call; import okhttp3.OkHttpClient; /** A simple model loader for fetching media over http/https using OkHttp. */ public class OkHttpUrlLoader implements ModelLoader { private final Call.Factory client; // Public API. @SuppressWarnings("WeakerAccess") public OkHttpUrlLoader(@NonNull Call.Factory client) { this.client = client; } @Override public boolean handles(@NonNull GlideUrl url) { return true; } @Override public LoadData buildLoadData( @NonNull GlideUrl model, int width, int height, @NonNull Options options) { return new LoadData<>(model, new OkHttpStreamFetcher(client, model)); } /** The default factory for {@link OkHttpUrlLoader}s. */ // Public API. @SuppressWarnings("WeakerAccess") public static class Factory implements ModelLoaderFactory { private static volatile Call.Factory internalClient; private final Call.Factory client; private static Call.Factory getInternalClient() { if (internalClient == null) { synchronized (Factory.class) { if (internalClient == null) { internalClient = new OkHttpClient(); } } } return internalClient; } /** Constructor for a new Factory that runs requests using a static singleton client. */ public Factory() { this(getInternalClient()); } /** * Constructor for a new Factory that runs requests using given client. * * @param client this is typically an instance of {@code OkHttpClient}. */ public Factory(@NonNull Call.Factory client) { this.client = client; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new OkHttpUrlLoader(client); } @Override public void teardown() { // Do nothing, this instance doesn't own the client. } } } ================================================ FILE: integration/okhttp4/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.okhttp" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.okhttp.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) annotationProcessor(project(":annotation:compiler")) api(libs.okhttp4) api(libs.androidx.annotation) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/okhttp4/gradle.properties ================================================ POM_NAME=Glide OkHttp 4.x Integration POM_ARTIFACT_ID=okhttp4-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library to use OkHttp 4.x to fetch data over http/https in Glide ================================================ FILE: integration/okhttp4/lint.xml ================================================ ================================================ FILE: integration/okhttp4/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpLibraryGlideModule.java ================================================ package com.bumptech.glide.integration.okhttp3; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.LibraryGlideModule; import java.io.InputStream; /** * Registers OkHttp related classes via Glide's annotation processor. * *

    For Applications that depend on this library and include an {@link AppGlideModule} and Glide's * annotation processor, this class will be automatically included. */ @GlideModule public final class OkHttpLibraryGlideModule extends LibraryGlideModule { @Override public void registerComponents( @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); } } ================================================ FILE: integration/okhttp4/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpStreamFetcher.java ================================================ package com.bumptech.glide.integration.okhttp3; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.HttpException; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.util.ContentLengthInputStream; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import java.io.InputStream; import java.util.Map; import okhttp3.Call; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; /** Fetches an {@link InputStream} using the okhttp library. */ public class OkHttpStreamFetcher implements DataFetcher, okhttp3.Callback { private static final String TAG = "OkHttpFetcher"; private final Call.Factory client; private final GlideUrl url; private InputStream stream; private ResponseBody responseBody; private DataCallback callback; // call may be accessed on the main thread while the object is in use on other threads. All other // accesses to variables may occur on different threads, but only one at a time. private volatile Call call; // Public API. @SuppressWarnings("WeakerAccess") public OkHttpStreamFetcher(Call.Factory client, GlideUrl url) { this.client = client; this.url = url; } @Override public void loadData( @NonNull Priority priority, @NonNull final DataCallback callback) { Request.Builder requestBuilder = new Request.Builder().url(url.toStringUrl()); for (Map.Entry headerEntry : url.getHeaders().entrySet()) { String key = headerEntry.getKey(); requestBuilder.addHeader(key, headerEntry.getValue()); } Request request = requestBuilder.build(); this.callback = callback; call = client.newCall(request); call.enqueue(this); } @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "OkHttp failed to obtain result", e); } callback.onLoadFailed(e); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) { responseBody = response.body(); if (response.isSuccessful()) { long contentLength = Preconditions.checkNotNull(responseBody).contentLength(); stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength); callback.onDataReady(stream); } else { callback.onLoadFailed(new HttpException(response.message(), response.code())); } } @Override public void cleanup() { try { if (stream != null) { stream.close(); } } catch (IOException e) { // Ignored } if (responseBody != null) { responseBody.close(); } callback = null; } @Override public void cancel() { Call local = call; if (local != null) { local.cancel(); } } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.REMOTE; } } ================================================ FILE: integration/okhttp4/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpUrlLoader.java ================================================ package com.bumptech.glide.integration.okhttp3; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import java.io.InputStream; import okhttp3.Call; import okhttp3.OkHttpClient; /** A simple model loader for fetching media over http/https using OkHttp. */ public class OkHttpUrlLoader implements ModelLoader { private final Call.Factory client; // Public API. @SuppressWarnings("WeakerAccess") public OkHttpUrlLoader(@NonNull Call.Factory client) { this.client = client; } @Override public boolean handles(@NonNull GlideUrl url) { return true; } @Override public LoadData buildLoadData( @NonNull GlideUrl model, int width, int height, @NonNull Options options) { return new LoadData<>(model, new OkHttpStreamFetcher(client, model)); } /** The default factory for {@link OkHttpUrlLoader}s. */ // Public API. @SuppressWarnings("WeakerAccess") public static class Factory implements ModelLoaderFactory { private static volatile Call.Factory internalClient; private final Call.Factory client; private static Call.Factory getInternalClient() { if (internalClient == null) { synchronized (Factory.class) { if (internalClient == null) { internalClient = new OkHttpClient(); } } } return internalClient; } /** Constructor for a new Factory that runs requests using a static singleton client. */ public Factory() { this(getInternalClient()); } /** * Constructor for a new Factory that runs requests using given client. * * @param client this is typically an instance of {@code OkHttpClient}. */ public Factory(@NonNull Call.Factory client) { this.client = client; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new OkHttpUrlLoader(client); } @Override public void teardown() { // Do nothing, this instance doesn't own the client. } } } ================================================ FILE: integration/recyclerview/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.recyclerview" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) compileOnly(libs.androidx.recyclerview) compileOnly(libs.androidx.fragment) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/recyclerview/gradle.properties ================================================ POM_NAME=Glide RecyclerView Integration POM_ARTIFACT_ID=recyclerview-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library to display images in RecyclerView. ================================================ FILE: integration/recyclerview/lint.xml ================================================ ================================================ FILE: integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListener.java ================================================ package com.bumptech.glide.integration.recyclerview; import android.widget.AbsListView; import android.widget.ListView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; /** * Converts {@link androidx.recyclerview.widget.RecyclerView.OnScrollListener} events to {@link * AbsListView} scroll events. * *

    Requires that the recycler view be using a {@link LinearLayoutManager} subclass. */ // Public API. @SuppressWarnings("WeakerAccess") public final class RecyclerToListViewScrollListener extends RecyclerView.OnScrollListener { public static final int UNKNOWN_SCROLL_STATE = Integer.MIN_VALUE; private final AbsListView.OnScrollListener scrollListener; private int lastFirstVisible = -1; private int lastVisibleCount = -1; private int lastItemCount = -1; public RecyclerToListViewScrollListener(@NonNull AbsListView.OnScrollListener scrollListener) { this.scrollListener = scrollListener; } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { int listViewState; switch (newState) { case RecyclerView.SCROLL_STATE_DRAGGING: listViewState = ListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL; break; case RecyclerView.SCROLL_STATE_IDLE: listViewState = ListView.OnScrollListener.SCROLL_STATE_IDLE; break; case RecyclerView.SCROLL_STATE_SETTLING: listViewState = ListView.OnScrollListener.SCROLL_STATE_FLING; break; default: listViewState = UNKNOWN_SCROLL_STATE; } scrollListener.onScrollStateChanged(null /*view*/, listViewState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); int firstVisible = layoutManager.findFirstVisibleItemPosition(); int visibleCount = Math.abs(firstVisible - layoutManager.findLastVisibleItemPosition()); int itemCount = recyclerView.getAdapter().getItemCount(); if (firstVisible != lastFirstVisible || visibleCount != lastVisibleCount || itemCount != lastItemCount) { scrollListener.onScroll(null, firstVisible, visibleCount, itemCount); lastFirstVisible = firstVisible; lastVisibleCount = visibleCount; lastItemCount = itemCount; } } } ================================================ FILE: integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPreloader.java ================================================ package com.bumptech.glide.integration.recyclerview; import android.app.Activity; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.ListPreloader; import com.bumptech.glide.ListPreloader.PreloadModelProvider; import com.bumptech.glide.ListPreloader.PreloadSizeProvider; import com.bumptech.glide.RequestManager; /** * Loads a few resources ahead in the direction of scrolling in any {@link RecyclerView} so that * images are in the memory cache just before the corresponding view in created in the list. Gives * the appearance of an infinitely large image cache, depending on scrolling speed, cpu speed, and * cache size. * *

    Must be added as a listener to the {@link RecyclerView} using {@link * RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener)}, or have its corresponding * methods called from another {@link androidx.recyclerview.widget.RecyclerView.OnScrollListener} to * function. * *

    This class only works with {@link androidx.recyclerview.widget.LinearLayoutManager} and * subclasses of {@link androidx.recyclerview.widget.LinearLayoutManager}. * * @param The type of the model being displayed in the {@link RecyclerView}. */ @SuppressWarnings("unused") public final class RecyclerViewPreloader extends RecyclerView.OnScrollListener { private final RecyclerToListViewScrollListener recyclerScrollListener; /** Helper constructor that accepts an {@link Activity}. */ public RecyclerViewPreloader( @NonNull Activity activity, @NonNull PreloadModelProvider preloadModelProvider, @NonNull PreloadSizeProvider preloadDimensionProvider, int maxPreload) { this(Glide.with(activity), preloadModelProvider, preloadDimensionProvider, maxPreload); } /** Helper constructor that accepts an {@link FragmentActivity}. */ public RecyclerViewPreloader( @NonNull FragmentActivity fragmentActivity, @NonNull PreloadModelProvider preloadModelProvider, @NonNull PreloadSizeProvider preloadDimensionProvider, int maxPreload) { this(Glide.with(fragmentActivity), preloadModelProvider, preloadDimensionProvider, maxPreload); } /** Helper constructor that accepts an {@link Fragment}. */ public RecyclerViewPreloader( @NonNull Fragment fragment, @NonNull PreloadModelProvider preloadModelProvider, @NonNull PreloadSizeProvider preloadDimensionProvider, int maxPreload) { this(Glide.with(fragment), preloadModelProvider, preloadDimensionProvider, maxPreload); } /** * Helper constructor that accepts an {@link android.app.Fragment}. * * @deprecated Use constructor RecyclerViewPreloader(Fragment, PreloadModelProvider, * PreloadSizeProvider) instead. */ @Deprecated public RecyclerViewPreloader( @NonNull android.app.Fragment fragment, @NonNull PreloadModelProvider preloadModelProvider, @NonNull PreloadSizeProvider preloadDimensionProvider, int maxPreload) { this(Glide.with(fragment), preloadModelProvider, preloadDimensionProvider, maxPreload); } /** * Constructor that accepts interfaces for providing the dimensions of images to preload, the list * of models to preload for a given position, and the request to use to load images. * * @param preloadModelProvider Provides models to load and requests capable of loading them. * @param preloadDimensionProvider Provides the dimensions of images to load. * @param maxPreload Maximum number of items to preload. */ public RecyclerViewPreloader( @NonNull RequestManager requestManager, @NonNull PreloadModelProvider preloadModelProvider, @NonNull PreloadSizeProvider preloadDimensionProvider, int maxPreload) { ListPreloader listPreloader = new ListPreloader<>( requestManager, preloadModelProvider, preloadDimensionProvider, maxPreload); recyclerScrollListener = new RecyclerToListViewScrollListener(listPreloader); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { recyclerScrollListener.onScrolled(recyclerView, dx, dy); } } ================================================ FILE: integration/sqljournaldiskcache/build.gradle.kts ================================================ plugins { id("com.android.library") id("kotlin-android") } android { namespace = "com.bumptech.glide.integration.sqljournaldiskcache" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) implementation(libs.errorprone.annotations) testImplementation(libs.guava.testlib) testImplementation(libs.truth) testImplementation(libs.junit) testImplementation(libs.mockito.core) testImplementation(libs.robolectric) testImplementation(libs.androidx.test.core) testImplementation(libs.androidx.junit) testImplementation(libs.androidx.test.runner) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/sqljournaldiskcache/gradle.properties ================================================ POM_NAME=Glide SQL Journaled Disk Cache POM_ARTIFACT_ID=sqljournaldiskcache POM_PACKAGING=aar POM_DESCRIPTION=A sql journaled LRU disk cache alternative to Glide's standard disk cache ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/Clock.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; /** * A simple wrapper for obtaining the current time for testing. * *

    While this interface exists in lots of libraries, especially internally at Google, there * doesn't seem to be a reasonable public version. For now we're just duplicating it again in Glide * so that the library can be open sourced. */ public interface Clock { long currentTimeMillis(); } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/DefaultClock.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; final class DefaultClock implements Clock { @Override public long currentTimeMillis() { return System.currentTimeMillis(); } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/DiskCacheDbHelper.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.annotation.SuppressLint; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import androidx.annotation.VisibleForTesting; /** The database helper for managing tables for {@link JournaledLruDiskCache}. */ final class DiskCacheDbHelper extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 2; // judds. private static final String DATABASE_NAME = "disk_cache"; static DiskCacheDbHelper forProd(Context context) { return new DiskCacheDbHelper(context, /* isInMemory= */ false); } static DiskCacheDbHelper forTesting(Context context) { return new DiskCacheDbHelper(context, /* isInMemory= */ true); } private DiskCacheDbHelper(Context context, boolean isInMemory) { this(context, isInMemory, DATABASE_VERSION); } @VisibleForTesting DiskCacheDbHelper(Context context, boolean isInMemory, int databaseVersion) { super(context, isInMemory ? null : DATABASE_NAME, /* factory= */ null, databaseVersion); setWriteAheadLoggingEnabled(true); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(JournalTable.getSqlCreateStatement()); db.execSQL(JournalTable.getIndexString()); db.execSQL(SizeTable.getSqlCreateStatement()); } @Override public void onOpen(SQLiteDatabase db) { db.execSQL("PRAGMA legacy_alter_table=ON"); db.setForeignKeyConstraintsEnabled(false); try { super.onOpen(db); } finally { db.setForeignKeyConstraintsEnabled(true); } } // We're matching the existing production behavior, which uses STRING even though it should use // TEXT @SuppressLint("SQLiteString") @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2) { // Dropping the journal table will also drop the index: https://sqlite.org/lang_droptable.html db.execSQL("DROP TABLE IF EXISTS journal"); db.execSQL("DROP TABLE IF EXISTS size"); db.execSQL( "CREATE TABLE journal(" + "key STRING PRIMARY KEY, " + "last_modified_time INTEGER NOT NULL, " + "pending_delete INTEGER NOT NULL DEFAULT 0, " + "size INTEGER NOT NULL" + ")"); db.execSQL( "CREATE INDEX journal_timestamp_key_idx" + " ON journal (" + "last_modified_time, " + "key" + ")"); db.execSQL( "CREATE TABLE size(" + "id INTEGER PRIMARY KEY, " + "size INTEGER NOT NULL DEFAULT 0" + ")"); } } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/EntryCache.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import androidx.collection.ArrayMap; import com.bumptech.glide.util.LruCache; import java.io.File; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Maintains an LRU cache of {@link String} keys to {@link Entry Entrys} where each entry contains a * read/write lock used to guarantee state and entries that are currently locked are guaranteed not * to be evicted. */ final class EntryCache { private final ArrayMap activeEntries = new ArrayMap<>(); private final LruCache inactiveEntries = new LruCache<>(6000); synchronized void clear() { activeEntries.clear(); inactiveEntries.clearMemory(); } synchronized Entry get(String key) { Entry entry = activeEntries.get(key); if (entry == null) { entry = inactiveEntries.get(key); if (entry == null) { entry = new Entry(key, this); activeEntries.put(key, entry); } } return entry; } private synchronized void removeFromActive(Entry entry) { activeEntries.remove(entry.key); inactiveEntries.put(entry.key, entry); } private synchronized void addToActive(Entry entry) { inactiveEntries.remove(entry.key); activeEntries.put(entry.key, entry); } static final class Entry { private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final String key; private final EntryCache cache; private int lockCount; private State state = State.UNKNOWN; private File file; Entry(String key, EntryCache cache) { this.key = key; this.cache = cache; } File getFile() { return file; } boolean isStateKnown() { return state != State.UNKNOWN; } boolean isPresent() { return state == State.PRESENT; } void setPresent(File file) { this.file = file; state = State.PRESENT; } void setUnknown() { state = State.UNKNOWN; } void setNotPresent() { state = State.NOT_PRESENT; } void acquireReadLock() { maybeSetActive(); readWriteLock.readLock().lock(); } void releaseReadLock() { ReentrantReadWriteLock lock = readWriteLock; maybeSetInactive(); lock.readLock().unlock(); } void acquireWriteLock() { maybeSetActive(); readWriteLock.writeLock().lock(); } void releaseWriteLock() { ReentrantReadWriteLock lock = readWriteLock; maybeSetInactive(); lock.writeLock().unlock(); } private synchronized void maybeSetActive() { lockCount++; if (lockCount == 1) { cache.addToActive(this); readWriteLock = new ReentrantReadWriteLock(); } } private synchronized void maybeSetInactive() { lockCount--; if (lockCount == 0) { cache.removeFromActive(this); readWriteLock = null; } } private enum State { UNKNOWN, PRESENT, NOT_PRESENT, } } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/EvictionManager.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import androidx.annotation.GuardedBy; import java.io.File; import java.util.List; final class EvictionManager { private static final String TAG = "Evictor"; // You must restart the app after enabling these logs for the change to take affect. // We cache isLoggable to avoid the performance hit of checking repeatedly. private static final boolean LOG_DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean LOG_VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); // The maximum amount we can go over our cache size before triggering evictions, currently 25mb. private static final long MAXIMUM_EVICTION_SLOP = 25 * 1024 * 1024; private final Handler evictionHandler; private final JournaledLruDiskCache diskCache; private final File cacheDirectory; private final FileSystem fileSystem; private final Journal journal; private final Looper workLooper; private final Clock clock; private final long evictionSlopBytes; private final long staleEvictionThresholdMs; @GuardedBy("this") private long maximumSizeBytes; EvictionManager( JournaledLruDiskCache diskCache, File cacheDirectory, FileSystem fileSystem, Journal journal, Looper workLooper, long maximumSizeBytes, float slopMultiplier, long staleEvictionThresholdMs, Clock clock) { this.diskCache = diskCache; this.cacheDirectory = cacheDirectory; this.fileSystem = fileSystem; this.journal = journal; this.workLooper = workLooper; this.maximumSizeBytes = maximumSizeBytes; this.clock = clock; this.staleEvictionThresholdMs = staleEvictionThresholdMs; evictionSlopBytes = Math.min(Math.round(maximumSizeBytes * slopMultiplier), MAXIMUM_EVICTION_SLOP); evictionHandler = new Handler(workLooper, new EvictionCallback()); } /** * Sets maximumSizeBytes to a new size. * *

    Must be called on a background thread. * *

    Decreasing the maximumSizeBytes may schedule an eviction if the current cache size exceeds * the new maximumSizeBytes. Evictions will be scheduled and executed asynchronously. Therefore, * the eviction will happen based on the latest maximum cache size, not the maximum size at * scheduling. */ synchronized void setMaximumSizeBytes(long newMaxSizeBytes) { long originalMaxBytes = maximumSizeBytes; maximumSizeBytes = newMaxSizeBytes; if (newMaxSizeBytes < originalMaxBytes) { maybeScheduleEviction(newMaxSizeBytes); } } private synchronized long getMaximumSizeBytes() { return maximumSizeBytes; } /** * Schedules a journal eviction on a work thread if the journal size currently exceeds the allowed * cache size. */ void maybeScheduleEviction() { maybeScheduleEviction(getMaximumSizeBytes()); } private void maybeScheduleEviction(long maximumSizeBytes) { if (isEvictionRequired(maximumSizeBytes)) { evictionHandler.obtainMessage(MessageIds.EVICT).sendToTarget(); } } private boolean isEvictionRequired(long maximumSizeBytes) { return journal.getCurrentSizeBytes() > evictionSlopBytes + maximumSizeBytes; } private void evictOnWorkThread() { if (!Looper.myLooper().equals(workLooper)) { throw new IllegalStateException( "Cannot call evictOnWorkThread on thread: " + Thread.currentThread().getName()); } long maximumSizeBytes = getMaximumSizeBytes(); long staleDateMs = clock.currentTimeMillis() - staleEvictionThresholdMs; List staleEntriesKeys = journal.getStaleEntries(staleDateMs); // Writes may queue up a number of eviction messages. After the first one runs, eviction may no // longer be necessary, so we simply ignore the message. if (!isEvictionRequired(maximumSizeBytes) && staleEntriesKeys.isEmpty()) { if (LOG_VERBOSE) { Log.v(TAG, "Ignoring eviction, not needed"); } return; } if (LOG_DEBUG) { Log.d(TAG, "Starting eviction on work thread"); } int successfullyDeletedCount = 0; int triedToDeleteEntries = staleEntriesKeys.size(); if (!staleEntriesKeys.isEmpty()) { successfullyDeletedCount += diskCache.delete(staleEntriesKeys).size(); } long targetSize = maximumSizeBytes - evictionSlopBytes; if (isEvictionRequired(maximumSizeBytes)) { long bytesToEvict = journal.getCurrentSizeBytes() - targetSize; List leastRecentlyUsedKeys = journal.getLeastRecentlyUsed(bytesToEvict); triedToDeleteEntries += leastRecentlyUsedKeys.size(); successfullyDeletedCount += diskCache.delete(leastRecentlyUsedKeys).size(); } if (triedToDeleteEntries == 0) { throw new IllegalStateException("Failed to find entries to evict."); } if (LOG_DEBUG) { Log.d( TAG, "Ran eviction" + ", tried to delete: " + triedToDeleteEntries + " entries" + ", actually deleted: " + successfullyDeletedCount + " entries" + ", target journal : " + targetSize + ", journal size: " + journal.getCurrentSizeBytes() + ", file size: " + fileSystem.getDirectorySize(cacheDirectory)); } } private class EvictionCallback implements Handler.Callback { @Override public boolean handleMessage(Message msg) { if (msg.what != MessageIds.EVICT) { return false; } evictOnWorkThread(); return true; } } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/FileSystem.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import java.io.File; import java.io.IOException; /** * Wraps a few common {@link File} methods to provide a few higher level functions and allow for * mocking uncommon error cases. */ interface FileSystem { default boolean delete(File file) { return file.delete(); } default boolean exists(File file) { return file.exists(); } default boolean createNewFile(File file) throws IOException { return file.createNewFile(); } default boolean rename(File from, File to) { return from.renameTo(to); } default long length(File file) { return file.length(); } default long getDirectorySize(File file) { long size = 0; if (file.isDirectory()) { for (File f : file.listFiles()) { size += getDirectorySize(f); } } else { size = file.length(); } return size; } default boolean deleteAll(File file) { boolean result = true; if (file.isDirectory()) { for (File f : file.listFiles()) { result = deleteAll(f) && result; } } else { result = file.delete(); } return result; } default boolean setLastModified(File file, long newLastModifiedTime) { return file.setLastModified(newLastModifiedTime); } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/GlideJournaledLruDiskCacheWrapper.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.SafeKeyGenerator; import com.bumptech.glide.util.Util; import java.io.File; /** Implements {@link DiskCache} using {@link JournaledLruDiskCache}. */ public final class GlideJournaledLruDiskCacheWrapper implements DiskCache { // 500 mb private static final long DEFAULT_GLIDE_CACHE_SIZE_BYTES = 1024 * 1024 * 500; public static final String DEFAULT_CACHE_DIR = "glide_cache"; public static final String KEY_VALUE_STORE_PREFIX = "com.google.android.apps.photos.diskcache.GlideJournaledLruDiskCacheWrapper"; private final JournaledLruDiskCache diskCache; private final SafeKeyGenerator safeKeyGenerator; private final DiskCacheDbHelper diskCacheDbHelper; public static GlideJournaledLruDiskCacheWrapper newInstance(Context context, File diskCacheDir) { return newInstance( context, diskCacheDir, // Default to not evicting based on entry age. /* staleEvictionThresholdMs= */ Long.MAX_VALUE, new DefaultClock()); } public static GlideJournaledLruDiskCacheWrapper newInstance( Context context, File diskCacheDir, long staleEvictionThresholdMs, Clock clock) { return new GlideJournaledLruDiskCacheWrapper( diskCacheDir, DiskCacheDbHelper.forProd(context), staleEvictionThresholdMs, clock); } private GlideJournaledLruDiskCacheWrapper( File diskCacheDir, DiskCacheDbHelper diskCacheDbHelper, long staleEvictionThresholdMs, Clock clock) { this.diskCacheDbHelper = diskCacheDbHelper; this.safeKeyGenerator = new SafeKeyGenerator(); this.diskCache = new JournaledLruDiskCache( diskCacheDir, diskCacheDbHelper, DEFAULT_GLIDE_CACHE_SIZE_BYTES, staleEvictionThresholdMs, clock); } /** * Sets the maximum size of the cache to a new size in bytes. * *

    Must be called on a background thread. * *

    The JournaledLruDiskCache manages the sizing of the cache. Decreasing the size may schedule * an eviction if the current cache size exceeds newMaximumSizeBytes. Evictions will be scheduled * and executed asynchronously. Therefore, the eviction will happen based on the latest maximum * cache size, not the maximum size at scheduling. */ public void setMaximumSizeBytes(long newMaximumSizeBytes) { Util.assertBackgroundThread(); diskCache.setMaximumSizeBytes(newMaximumSizeBytes); } @Override public File get(Key key) { String safeKey = safeKeyGenerator.getSafeKey(key); return diskCache.get(safeKey); } @Override public void put(Key key, Writer writer) { String safeKey = safeKeyGenerator.getSafeKey(key); File tempFile = diskCache.beginPut(safeKey); // Edit already in progress, or file is already written. try { if (tempFile != null && writer.write(tempFile)) { diskCache.commitPut(safeKey, tempFile); } } finally { diskCache.abortPutIfNotCommitted(safeKey, tempFile); } } @Override public void delete(Key key) { String safeKey = safeKeyGenerator.getSafeKey(key); diskCache.delete(safeKey); } @Override public void clear() { diskCache.clear(); } /** * @deprecated this method will be replaced by a more specific version */ @Deprecated public SQLiteDatabase getWritableDatabase() { return diskCacheDbHelper.getWritableDatabase(); } /** Returns number of bytes used by the JournaledLruDiskCache currently. */ public long getCurrentSizeBytes() { return diskCache.getCurrentSizeBytes(); } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/Journal.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.text.TextUtils; import android.util.Log; import com.bumptech.glide.integration.sqljournaldiskcache.SizeJournal.SizeSQLiteTransactionListener; import com.bumptech.glide.util.Preconditions; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; final class Journal { private static final String TAG = "Journal"; // You must restart the app after enabling these logs for the change to take affect. // We cache isLoggable to avoid the performance hit of checking repeatedly. private static final boolean LOG_VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); private static final boolean LOG_DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean LOG_WARN = Log.isLoggable(TAG, Log.WARN); private static final String ROW_ID = "rowid"; private static final String WHERE_KEY = JournalTable.Columns.KEY + " = ?"; private static final String WHERE_PENDING_DELETE = JournalTable.Columns.PENDING_DELETE + " != 0"; // If a commit fails (renameTo returns false) and then the app dies before the commit is aborted, // we will end up with a temp file and an entry in the journal for a key. New puts for that key // should be able to complete successfully, so we use insert or replace to allow the entry to be // updated. We don't normally expect to be replacing entries. private static final String INSERT_NEW_KEY_SQL = "INSERT OR REPLACE INTO " + JournalTable.TABLE_NAME + "(" + JournalTable.Columns.KEY + ", " + JournalTable.Columns.LAST_MODIFIED_TIME + ", " + JournalTable.Columns.SIZE + ") VALUES (?, ?, ?)"; private static final int INSERT_NEW_KEY_KEY_IDX = 1; private static final int INSERT_NEW_KEY_MODIFIED_TIME_IDX = 2; private static final int INSERT_NEW_KEY_SIZE_IDX = 3; private static final String CONTAINS_KEY_SQL = "SELECT COUNT(*) FROM " + JournalTable.TABLE_NAME + " WHERE " + WHERE_KEY; private static final int CONTAINS_KEY_KEY_IDX = 1; private static final String SELECT_ENTRY_SIZE_NOT_PENDING_SQL = "SELECT " + JournalTable.Columns.SIZE + " FROM " + JournalTable.TABLE_NAME + " WHERE " + WHERE_KEY + " AND " + JournalTable.Columns.PENDING_DELETE + " = 0"; private static final int SELECT_ENTRY_SIZE_NOT_PENDING_KEY_IDX = 1; private static final String DELETE_ENTRY_SQL = "DELETE FROM " + JournalTable.TABLE_NAME + " WHERE " + WHERE_KEY; private static final int DELETE_ENTRY_KEY_IDX = 1; private static final String[] LRU_PROJECTION = new String[] {JournalTable.Columns.KEY, JournalTable.Columns.SIZE}; private static final String[] STALE_PROJECTION = new String[] {JournalTable.Columns.KEY, JournalTable.Columns.LAST_MODIFIED_TIME, ROW_ID}; private static final String LRU_WHERE = JournalTable.Columns.PENDING_DELETE + " = 0"; private static final String STALE_WHERE = ROW_ID + " > ? AND " + JournalTable.Columns.LAST_MODIFIED_TIME + " < ?"; // rowid comes from https://www.sqlite.org/rowidtable.html. See b/206890186. private static final String LRU_ORDER_BY = JournalTable.Columns.LAST_MODIFIED_TIME + " ASC, rowid ASC"; private static final String STALE_ORDER_BY = ROW_ID + " ASC"; private static final int LRU_BATCH_SIZE = 25; private static final int STALE_BATCH_SIZE = 25; private static final String SUM_SIZE_WHERE_NOT_PENDING_DELETE = "SELECT SUM(" + JournalTable.Columns.SIZE + ") FROM " + JournalTable.TABLE_NAME + " WHERE " + JournalTable.Columns.PENDING_DELETE + " = 0"; private static final String[] PENDING_DELETE_PROJECTION = new String[] {JournalTable.Columns.KEY}; private static final int DELETE_BATCH_SIZE = 200; private final DiskCacheDbHelper dbHelper; private final SqliteStatementPool statementPool; private final Clock clock; private final Handler updateTimesHandler; private final SizeJournal sizeJournal; Journal( DiskCacheDbHelper dbHelper, Looper workThreadLooper, int updateModifiedTimeBatchSize, Clock clock) { this.dbHelper = dbHelper; this.sizeJournal = new SizeJournal(dbHelper); statementPool = new SqliteStatementPool(dbHelper); this.clock = clock; updateTimesHandler = new Handler( workThreadLooper, new UpdateTimesCallback(dbHelper, updateModifiedTimeBatchSize, clock)); } long getCurrentSizeBytes() { return sizeJournal.getCacheSizeBytes(); } void open() { sizeJournal.open(); } void clear() { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.beginTransactionNonExclusive(); try { db.delete(JournalTable.TABLE_NAME, null /*whereClause*/, null /*whereArgs*/); sizeJournal.clearInTransaction(); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } void get(String key) { updateTimesHandler.obtainMessage(MessageIds.ADD_LAST_MODIFIED_KEY, key).sendToTarget(); } void put(String key, long sizeBytes) { Preconditions.checkArgument(!TextUtils.isEmpty(key)); SQLiteDatabase db = dbHelper.getWritableDatabase(); SQLiteStatement insertStatement = statementPool.obtain(INSERT_NEW_KEY_SQL); insertStatement.bindString(INSERT_NEW_KEY_KEY_IDX, key); insertStatement.bindLong(INSERT_NEW_KEY_MODIFIED_TIME_IDX, clock.currentTimeMillis()); insertStatement.bindLong(INSERT_NEW_KEY_SIZE_IDX, sizeBytes); SizeSQLiteTransactionListener sizeListener = sizeJournal.prepareSizeTransaction(); db.beginTransactionWithListenerNonExclusive(sizeListener); try { insertStatement.executeInsert(); sizeJournal.incrementSizeInTransaction(sizeListener, sizeBytes); db.setTransactionSuccessful(); } finally { db.endTransaction(); statementPool.offer(INSERT_NEW_KEY_SQL, insertStatement); sizeJournal.endSizeTransaction(sizeListener); } } List getPendingDeleteKeys() { List result = new ArrayList<>(); SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db.query( JournalTable.TABLE_NAME, PENDING_DELETE_PROJECTION, WHERE_PENDING_DELETE, null /*selectionArgs*/, null /*groupBy*/, null /*having*/, null /*orderBy*/); try { while (cursor.moveToNext()) { String key = cursor.getString(cursor.getColumnIndexOrThrow(JournalTable.Columns.KEY)); if (TextUtils.isEmpty(key)) { if (LOG_WARN) { Log.w(TAG, "Found empty or null key: %s, skipping delete: " + key); } } else { result.add(key); } } } finally { cursor.close(); } return result; } List getLeastRecentlyUsed(long targetByteCount) { SQLiteDatabase db = dbHelper.getReadableDatabase(); List keys = new ArrayList<>(); long currentByteCount = 0; int currentOffset = 0; boolean isOutOfEntries = false; while (!isOutOfEntries && currentByteCount < targetByteCount) { Cursor cursor = db.query( JournalTable.TABLE_NAME, LRU_PROJECTION, LRU_WHERE, null /*selectionArgs*/, null /*groupBy*/, null /*having*/, LRU_ORDER_BY, currentOffset + ", " + LRU_BATCH_SIZE); try { int keyIdx = cursor.getColumnIndexOrThrow(JournalTable.Columns.KEY); int sizeIdx = cursor.getColumnIndexOrThrow(JournalTable.Columns.SIZE); while (cursor.moveToNext() && currentByteCount < targetByteCount) { String key = cursor.getString(keyIdx); keys.add(key); long sizeBytes = cursor.getLong(sizeIdx); currentByteCount += sizeBytes; } isOutOfEntries = cursor.getCount() < LRU_BATCH_SIZE; } finally { cursor.close(); } currentOffset += LRU_BATCH_SIZE; } // TODO(judds): for a sufficiently large file or small cache size and a failed attempt to commit // a put, this can happen because our journal size will temporarily not match our File size. // If this becomes an issue, we can safely just clear the cache here instead of throwing because // we were about to delete all the files anyway. if (isOutOfEntries && currentByteCount < targetByteCount) { throw new IllegalStateException( "Size mismatch" + ", expected to be able to evict at least " + targetByteCount + " bytes" + ", but only found " + currentByteCount + " bytes worth of entries!"); } return keys; } List getStaleEntries(long staleTimeThresholdMs) { SQLiteDatabase db = dbHelper.getReadableDatabase(); List keys = new ArrayList<>(); long currentRowId = 0L; boolean isOutOfEntries = false; while (!isOutOfEntries) { try (Cursor cursor = db.query( JournalTable.TABLE_NAME, STALE_PROJECTION, LRU_WHERE + " AND " + STALE_WHERE, new String[] {String.valueOf(currentRowId), String.valueOf(staleTimeThresholdMs)}, null /*groupBy*/, null /*having*/, STALE_ORDER_BY, String.valueOf(STALE_BATCH_SIZE))) { int keyIdx = cursor.getColumnIndexOrThrow(JournalTable.Columns.KEY); while (cursor.moveToNext()) { keys.add(cursor.getString(keyIdx)); currentRowId = cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)); } isOutOfEntries = cursor.getCount() < STALE_BATCH_SIZE; } } return keys; } /** * Removes the pending entry from the journal for the given key and returns the size in bytes of * the entry, or returns 0 if no such entry exists. */ void abortPut(String key) { SQLiteDatabase db = dbHelper.getWritableDatabase(); SQLiteStatement containsKeyStatement = statementPool.obtain(CONTAINS_KEY_SQL); containsKeyStatement.bindString(CONTAINS_KEY_KEY_IDX, key); SQLiteStatement selectNotPendingSizeStatement = statementPool.obtain(SELECT_ENTRY_SIZE_NOT_PENDING_SQL); selectNotPendingSizeStatement.bindString(SELECT_ENTRY_SIZE_NOT_PENDING_KEY_IDX, key); SQLiteStatement deleteEntryStatement = statementPool.obtain(DELETE_ENTRY_SQL); deleteEntryStatement.bindString(DELETE_ENTRY_KEY_IDX, key); SizeSQLiteTransactionListener sizeListener = sizeJournal.prepareSizeTransaction(); db.beginTransactionWithListenerNonExclusive(sizeListener); try { // We may be asked to abort a put that failed before the entry was updated, so we will // occasionally fail to find the entry here. boolean isKeyPresent = 0 != containsKeyStatement.simpleQueryForLong(); if (!isKeyPresent) { return; } long entrySize; try { entrySize = selectNotPendingSizeStatement.simpleQueryForLong(); } catch (SQLiteDoneException e) { // No row found for this key that is not pending delete. entrySize = 0; } int deleted = deleteEntryStatement.executeUpdateDelete(); if (deleted != 1) { throw new IllegalStateException( "Failed to delete entry" + ", key: " + key + ", size: " + entrySize + ", actually deleted: " + deleted); } else { // If the item is pending delete its size is 0 here - skip decrementing. if (entrySize != 0) { sizeJournal.decrementSizeInTransaction(sizeListener, entrySize); } } db.setTransactionSuccessful(); } finally { db.endTransaction(); statementPool.offer(CONTAINS_KEY_SQL, containsKeyStatement); statementPool.offer(SELECT_ENTRY_SIZE_NOT_PENDING_SQL, selectNotPendingSizeStatement); statementPool.offer(DELETE_ENTRY_SQL, deleteEntryStatement); sizeJournal.endSizeTransaction(sizeListener); } } void delete(List keys) { SQLiteDatabase db = dbHelper.getWritableDatabase(); for (int startPosition = 0; startPosition < keys.size(); startPosition += DELETE_BATCH_SIZE) { int endPosition = Math.min(keys.size(), startPosition + DELETE_BATCH_SIZE); List batch = keys.subList(startPosition, endPosition); int batchSize = batch.size(); if (batchSize == 0) { if (LOG_WARN) { Log.w( TAG, "Unexpectedly 0 sized batch between: " + startPosition + " and endPosition: " + endPosition); } continue; } String[] keysToDelete = batch.toArray(new String[batchSize]); db.beginTransactionNonExclusive(); try { int deleted = db.delete(JournalTable.TABLE_NAME, buildKeySelectionSet(batchSize), keysToDelete); if (deleted != keysToDelete.length && LOG_WARN) { Log.w( TAG, "Failed to delete all expected entries" + ", expected: " + keysToDelete.length + ", deleted: " + deleted); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } } private static String buildKeySelectionSet(int count) { Preconditions.checkArgument(count > 0); String prefix = " IN("; String postfix = "?)"; // (?,?,?), so 2 characters per item, except for the last one which is one character. int commaSeparatedCount = count - 1; StringBuilder sb = new StringBuilder( JournalTable.Columns.KEY.length() + prefix.length() + (2 * commaSeparatedCount) + postfix.length()) .append(JournalTable.Columns.KEY) .append(prefix); for (int i = 0; i < commaSeparatedCount; i++) { sb.append("?,"); } return sb.append(postfix).toString(); } void markPendingDelete(List keys) { ContentValues values = new ContentValues(); values.put(JournalTable.Columns.PENDING_DELETE, 1); SQLiteDatabase db = dbHelper.getWritableDatabase(); for (int startPosition = 0; startPosition < keys.size(); startPosition += DELETE_BATCH_SIZE) { int endPosition = Math.min(keys.size(), startPosition + DELETE_BATCH_SIZE); List batch = keys.subList(startPosition, endPosition); int batchSize = batch.size(); String[] keysToDelete = batch.toArray(new String[batchSize]); String keySelectionSet = buildKeySelectionSet(batchSize); SizeSQLiteTransactionListener sizeListener = sizeJournal.prepareSizeTransaction(); db.beginTransactionWithListenerNonExclusive(sizeListener); try { long sumOfSizesOfNewlyPendingEntries = DatabaseUtils.longForQuery( db, SUM_SIZE_WHERE_NOT_PENDING_DELETE + " AND " + keySelectionSet, keysToDelete); sizeJournal.decrementSizeInTransaction(sizeListener, sumOfSizesOfNewlyPendingEntries); db.update(JournalTable.TABLE_NAME, values, keySelectionSet, keysToDelete); db.setTransactionSuccessful(); } finally { db.endTransaction(); sizeJournal.endSizeTransaction(sizeListener); } } } private static class UpdateTimesCallback implements Handler.Callback { private final SQLiteOpenHelper dbHelper; private final int updateModifiedTimeBatchSize; private final List keysToUpdate; private final String batchUpdatedModifiedTimeSql; private final Clock clock; private SQLiteStatement sqlStatement; UpdateTimesCallback(SQLiteOpenHelper dbHelper, int updateModifiedTimeBatchSize, Clock clock) { this.clock = clock; Preconditions.checkArgument(updateModifiedTimeBatchSize > 0); this.dbHelper = dbHelper; this.updateModifiedTimeBatchSize = updateModifiedTimeBatchSize; keysToUpdate = new ArrayList<>(updateModifiedTimeBatchSize); batchUpdatedModifiedTimeSql = "UPDATE " + JournalTable.TABLE_NAME + " SET " + JournalTable.Columns.LAST_MODIFIED_TIME + " = ?" + " WHERE " + buildKeySelectionSet(updateModifiedTimeBatchSize); } private SQLiteStatement getSqlStatement() { if (sqlStatement == null) { sqlStatement = dbHelper.getWritableDatabase().compileStatement(batchUpdatedModifiedTimeSql); } return sqlStatement; } private void updateTimes() { long startTime = clock.currentTimeMillis(); SQLiteDatabase db = dbHelper.getWritableDatabase(); SQLiteStatement statement = getSqlStatement(); long modifiedTime = clock.currentTimeMillis(); statement.bindLong(1, modifiedTime); int size = keysToUpdate.size(); for (int i = 0; i < size; i++) { String key = keysToUpdate.get(i); // 1 indexed, with the modified time as the first argument. statement.bindString(i + 2, key); } db.beginTransactionNonExclusive(); try { int updated = statement.executeUpdateDelete(); if (updated != updateModifiedTimeBatchSize && LOG_DEBUG) { Set uniqueKeys = new HashSet<>(keysToUpdate); // This can happen in one of two cases: // 1. Files are deleted out from under us (by the system), triggering a cache rebuild. // 2. The corresponding entries are evicted while they're in the get queue. Log.d( TAG, "Failed to update modified time for all rows" + ", time: " + modifiedTime + ", expected: " + updateModifiedTimeBatchSize + ", actually updated: " + updated + ", unique keys: " + uniqueKeys.size()); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } if (LOG_VERBOSE) { Log.v( TAG, "Completed update times with " + keysToUpdate.size() + " updates in " + (clock.currentTimeMillis() - startTime)); } } @Override public boolean handleMessage(Message msg) { if (msg.what != MessageIds.ADD_LAST_MODIFIED_KEY) { return false; } String updatedKey = (String) msg.obj; if (!keysToUpdate.contains(updatedKey)) { keysToUpdate.add(updatedKey); } if (keysToUpdate.size() == updateModifiedTimeBatchSize) { updateTimes(); keysToUpdate.clear(); } return true; } } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/JournalTable.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; final class JournalTable { static final String TABLE_NAME = "journal"; private static final String INDEX_TIMESTAMP_KEY = "journal_timestamp_key_idx"; interface Columns { /** The cache key/cache file name. */ String KEY = "key"; /** The time the key was most recently created, updated, or read in UTC milliseconds. */ String LAST_MODIFIED_TIME = "last_modified_time"; /** 1 if the key is going to be deleted, 0 otherwise. */ String PENDING_DELETE = "pending_delete"; /** The length in bytes of the cache file. */ String SIZE = "size"; } static String getSqlCreateStatement() { return "CREATE TABLE " + TABLE_NAME + " (" + Columns.KEY + " STRING PRIMARY KEY, " + Columns.LAST_MODIFIED_TIME + " INTEGER NOT NULL, " + Columns.PENDING_DELETE + " INTEGER NOT NULL DEFAULT 0, " + Columns.SIZE + " INTEGER NOT NULL" + ")"; } static String getIndexString() { return "CREATE INDEX " + INDEX_TIMESTAMP_KEY + " ON " + TABLE_NAME + " (" + Columns.LAST_MODIFIED_TIME + ", " + Columns.KEY + ")"; } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/JournaledLruDiskCache.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Preconditions; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * An Lru disk cache that stores each entry as a single File and uses a SQL based journal to track * sizes and eviction order. * *

    Operations are not guaranteed and will silently fail in unexpected cases. * *

    The size of the cache is approximate and will be exceeded for short periods of time. Failure * cases may leave behind temporary files that should be cleaned up in the future when the cache is * re-opened or when operations are attempted again. * *

    This class is thread safe and may be accessed from multiple threads simultaneously. */ final class JournaledLruDiskCache { private static final String TAG = "DiskCache"; private static final String CANARY_FILE_NAME = "cache_canary"; // You must restart the app after enabling these logs for the change to take affect. // We cache isLoggable to avoid the performance hit of checking repeatedly. private static final boolean LOG_WARN = Log.isLoggable(TAG, Log.WARN); private static final boolean LOG_VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); // The fraction of the maximum byte size of the cache we will allow the cache to go over before // triggering an eviction. private static final float DEFAULT_EVICTION_SLOP_MULTIPLIER = 0.05f; // The number of items we will queue to update the date modified time of in batches. private static final int DEFAULT_UPDATE_MODIFIED_TIME_BATCH_SIZE = 20; static final String TEMP_FILE_INDICATOR = ".tmp"; private final File cacheDirectory; private final FileSystem fileSystem; private final Journal journal; // We use this File to determine if the system has wiped out our cache directory, which it may do // at any time. If the File is not present, then either we've never opened the cache for the given // directory before, or the cache was wiped. private final File canaryFile; private final EvictionManager evictionManager; private final RecoveryManager recoveryManager; private final EntryCache entries = new EntryCache(); private volatile boolean isOpen; /** * @param cacheDirectory The directory in which the cache should store its files (Warning: the * cache will delete all Files in the given directory. The directory should not be used to * store any other content). * @param maximumSizeBytes The target maximum size in bytes. The cache size may briefly exceed * this size by up to around 25mb depending on the size, thread scheduling, and the number of * failed requests. */ JournaledLruDiskCache( File cacheDirectory, DiskCacheDbHelper diskCacheDbHelper, long maximumSizeBytes, long staleEvictionThresholdMs, Clock clock) { this( cacheDirectory, diskCacheDbHelper, new FileSystem() {}, maximumSizeBytes, getBackgroundLooper(), DEFAULT_EVICTION_SLOP_MULTIPLIER, DEFAULT_UPDATE_MODIFIED_TIME_BATCH_SIZE, staleEvictionThresholdMs, clock); } @VisibleForTesting JournaledLruDiskCache( File cacheDirectory, DiskCacheDbHelper diskCacheDbHelper, FileSystem fileSystem, long maximumSizeBytes, Looper workLooper, float slopMultiplier, int updateModifiedTimeBatchSize, long staleEvictionThresholdMs, Clock clock) { Preconditions.checkArgument( updateModifiedTimeBatchSize >= 1, "updated modified time batch size must be >= 1"); this.cacheDirectory = cacheDirectory; this.fileSystem = fileSystem; journal = new Journal(diskCacheDbHelper, workLooper, updateModifiedTimeBatchSize, clock); canaryFile = new File(cacheDirectory, CANARY_FILE_NAME); evictionManager = new EvictionManager( this, cacheDirectory, fileSystem, journal, workLooper, maximumSizeBytes, slopMultiplier, staleEvictionThresholdMs, clock); recoveryManager = new RecoveryManager(this, cacheDirectory, journal, workLooper); } private static Looper getBackgroundLooper() { HandlerThread workThread = new HandlerThread("disk_cache_journal", Process.THREAD_PRIORITY_BACKGROUND); workThread.start(); return workThread.getLooper(); } @SuppressWarnings("checkstyle:UnnecessaryParentheses") // Readability private void openIfNotOpen() { if (!isOpen) { synchronized (this) { if (!isOpen) { boolean createdDirectory = cacheDirectory.mkdirs() || (cacheDirectory.exists() && cacheDirectory.isDirectory()); if (!createdDirectory) { throw new IllegalStateException("Failed to create cache directory: " + cacheDirectory); } journal.open(); isOpen = true; recoveryManager.triggerRecovery(); } } } } // TODO(judds): rather than polling, we should use Android's FileObserver. private void verifyCanaryOrClear() { if (fileSystem.exists(canaryFile)) { return; } synchronized (this) { if (fileSystem.exists(canaryFile)) { return; } if (LOG_WARN) { Log.w(TAG, "Failed to find canary file, clearing disk cache"); } clear(); } } private void touchCanaryFile() { try { if (!fileSystem.createNewFile(canaryFile) && LOG_WARN) { Log.w(TAG, "Failed to create new canary file"); } } catch (IOException e) { if (LOG_WARN) { Log.w(TAG, "Threw creating canary", e); } } } long getCurrentSizeBytes() { return journal.getCurrentSizeBytes(); } /** * Makes a best effort attempt to delete all Files and clear the journal. * *

    In progress writes may still complete and/or leave behind partial data. */ public synchronized void clear() { if (LOG_WARN) { Log.w(TAG, "Clearing cache and deleting all entries!"); } fileSystem.deleteAll(cacheDirectory); journal.clear(); isOpen = false; entries.clear(); openIfNotOpen(); touchCanaryFile(); } /** * Attempts to delete any content currently in the cache for the given key. * *

    If no entry for the given key is found, this method will silently fail. If an entry is * found, it is possible the File deletion will fail and be re-attempted in the future. */ public void delete(String key) { delete(Collections.singletonList(key)); } List delete(List keys) { journal.markPendingDelete(keys); List successfullyDeleted = new ArrayList<>(keys.size()); for (String key : keys) { EntryCache.Entry entry = entries.get(key); entry.acquireWriteLock(); try { File file = getCacheFile(key); if (fileSystem.delete(file)) { successfullyDeleted.add(key); } else if (LOG_WARN) { Log.w(TAG, "Failed to delete file: " + file); } entry.setNotPresent(); } finally { entry.releaseWriteLock(); } } journal.delete(successfullyDeleted); return successfullyDeleted; } /** * Returns a File committed previously for the given key, or {@code null} if no such File exists. * *

    If a write is in progress but not yet committed for the given key, this method will return * {@code null} immediately, just as if the key were simply not present. */ public File get(String key) { long startTime = getLogTime(); openIfNotOpen(); final File result; EntryCache.Entry entry = entries.get(key); entry.acquireReadLock(); try { if (entry.isStateKnown()) { result = entry.isPresent() ? entry.getFile() : null; } else { File cacheFile = getCacheFile(key); if (fileSystem.exists(cacheFile)) { entry.setPresent(cacheFile); result = cacheFile; } else { entry.setNotPresent(); result = null; } } if (result != null) { journal.get(key); } if (LOG_VERBOSE) { Log.v(TAG, "Completed get in: " + getElapsedTime(startTime) + ", key: " + key); } } finally { entry.releaseReadLock(); } return result; } /** * Starts a put for the given key and returns a temporary {@link File} to which the caller can * write data, or {@code null} if an edit is already in progress for the given Key, or if a * committed entry already exists for the given key. * *

    Callers should call {@link #commitPut(String, File)} with the given key and the {@link File} * returned from this method after they finish writing data to make the data they have written * available to calls to {@link #get(String)}. If an error occurs while writing data, callers can * omit calling {@link #commitPut(String, File)} and use {@link #abortPutIfNotCommitted(String, * File)} to cleanup any partial {@link File Files}. * *

    Callers must call {@link #abortPutIfNotCommitted(String, File)} regardless of whether or not * their write succeeds. The expected pattern is as follows: * *

    {@code
       * File tempFile = cache.beginPut(key);
       * try {
       *   if (tempFile != null && writeToFile(someData, tempFile)) {
       *    cache.commitPut(key, tempFile);
       *   }
       * } finally {
       *   cache.abortIfNotCommitted(key, tempFile);
       * }
       * }
    * *

    Until the caller calls {@link #abortPutIfNotCommitted(String, File)}, a lock is held that * will block future calls to this method for the given key. * *

    The returned {@link File} may contain partial data if a previous write to this key failed. * Callers should not assume it is safe to append to the File without first clearing it. */ @Nullable public File beginPut(String key) { long startTime = getLogTime(); openIfNotOpen(); verifyCanaryOrClear(); EntryCache.Entry entry = entries.get(key); entry.acquireWriteLock(); File permanentFile = getCacheFile(key); if (fileSystem.exists(permanentFile)) { return null; } File result = getTempFile(key); if (LOG_VERBOSE) { Log.v(TAG, "Completed begin put in: " + getElapsedTime(startTime) + ", key: " + key); } return result; } /** * Updates the size of the cache based on the data in the given temporary file and renames the * given temporary File to its permanent equivalent and makes it available to calls from {@link * #get(String)}. * *

    The given {@link File} must be a {@link File} returned from {@link #get(String)} for the * given key. No validation is performed to verify either that the given {@link File} is a * legitimate temporary file from this cache or that the given {@link File} matches the given key. * *

    It is possible this commit may fail silently, there is no guarantee that the data in the * given {@link File} will actually be available from {@link #get(String)}} when this method * completes. In practice commits should fail rarely unless insufficient storage is available or * the cache's directory or files are manipulated by a third party. * *

    If the commit does fail, it will do so in one of two ways: * *

      *
    • Prior to or while writing the entry to the journal *
    • After writing the entry to the journal prior to or while renaming the temporary file to * the permanent file. *
    * * If the commit fails prior to writing the entry to the journal, the dangling temporary File will * be found during recovery and deleted. If the commit fails after writing the entry to the * journal, the temporary file will be found during recovery and deleted and the corresponding * journal entry will also be deleted. The absence of a temporary File for a given key is assumed * to mean that either no entry exists, or the entry is committed and may be read. * * @throws IllegalStateException If this method wasn't preceded by a call to {@link * #beginPut(String)} for the given key. */ public void commitPut(String key, File temp) { long startTime = getLogTime(); long totalBytesAdded = fileSystem.length(temp); journal.put(key, totalBytesAdded); if (LOG_VERBOSE) { Log.v(TAG, "Completed insertIntoDb in: " + getElapsedTime(startTime)); } long startRenameTime = getLogTime(); File permanentFile = getCacheFile(key); boolean isRenameSuccessful = fileSystem.rename(temp, permanentFile); // If we fail to rename the file, we will try to recover in our next recovery phase. if (isRenameSuccessful) { if (LOG_VERBOSE) { Log.v(TAG, "Successfully renamed in: " + getElapsedTime(startRenameTime)); } EntryCache.Entry entry = entries.get(key); entry.setPresent(permanentFile); } else if (LOG_WARN) { Log.w(TAG, "Failed to rename file" + ", from: " + temp + ", to: " + permanentFile); } evictionManager.maybeScheduleEviction(); if (LOG_VERBOSE) { Log.v( TAG, "Completed commitPut in: " + getElapsedTime(startTime) + ", current size: " + journal.getCurrentSizeBytes() + ", key: " + key); } } /** * Releases the write lock for the given key and, if the write was not committed, cleans up the * given temporary File and the corresponding journal entry for the given Key. * *

    A write is assumed to have not been committed if the given temporary File still exists. */ public void abortPutIfNotCommitted(String key, File temp) { try { // If the temporary File still exists, we haven't committed. If it doesn't exist, we either // didn't start writing and have nothing to roll back, or we finished writing and finished // the rename so the edit is committed. if (temp != null && fileSystem.delete(temp)) { journal.abortPut(key); EntryCache.Entry entry = entries.get(key); entry.setUnknown(); } } finally { EntryCache.Entry entry = entries.get(key); entry.releaseWriteLock(); } } void recoverPartialWrite(File temp) { String key = keyFromFile(temp); EntryCache.Entry entry = entries.get(key); entry.acquireWriteLock(); try { // Try to delete the temporary file, if it fails, we will try again in the next recovery // phase. boolean deleted = temp.delete(); if (!deleted) { if (LOG_WARN) { Log.w(TAG, "Failed to cleanup in progress write: " + temp); } // The write lock prevents us from directly racing with an in progress write. However when // the write lock is released, we will get to run. If the write completed successfully, // the // temp file will no longer exist, but the entry will. We do not want to delete the entry // just because we happened to try to run recovery during the write. return; } delete(key); } finally { entry.releaseWriteLock(); } } private String keyFromFile(File file) { String name = file.getName(); final String key; if (name.endsWith(TEMP_FILE_INDICATOR)) { key = name.substring(0, name.length() - TEMP_FILE_INDICATOR.length()); } else { key = name; } return key; } /** * Sets the maximum size of the cache to a new size in bytes. * *

    Must be called on a background thread. * *

    The EvictionManager manages the sizing of the cache. Decreasing the size may schedule an * eviction if the current cache size exceeds newMaximumSizeBytes. Evictions will be scheduled and * executed asynchronously. Therefore, the eviction will happen based on the latest maximum cache * size, not the maximum size at scheduling. */ public void setMaximumSizeBytes(long newMaximumSizeBytes) { evictionManager.setMaximumSizeBytes(newMaximumSizeBytes); } private File getCacheFile(String key) { return new File(cacheDirectory, key); } private File getTempFile(String key) { return new File(cacheDirectory, key + TEMP_FILE_INDICATOR); } private static long getLogTime() { return System.currentTimeMillis(); } private static long getElapsedTime(long startTime) { return getLogTime() - startTime; } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/MessageIds.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; /** * A unique set of non-zero message ids to use when requesting that work be done on the disk cache's * background thread. */ interface MessageIds { int ADD_LAST_MODIFIED_KEY = 1; int EVICT = 2; int RECOVER = 3; } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/RecoveryManager.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.os.Handler; import android.os.Looper; import android.os.Message; import java.io.File; import java.io.FilenameFilter; import java.util.List; /** Finds and cleans up failed writes and deletes on the work thread. */ final class RecoveryManager { private final JournaledLruDiskCache diskCache; private final Journal journal; private final File diskCacheDir; private final Looper workLooper; private final Handler recoveryHandler; RecoveryManager( JournaledLruDiskCache diskCache, File diskCacheDir, Journal journal, Looper workLooper) { this.diskCache = diskCache; this.journal = journal; this.diskCacheDir = diskCacheDir; this.workLooper = workLooper; recoveryHandler = new Handler(workLooper, new RecoveryCallback()); } void triggerRecovery() { recoveryHandler.obtainMessage(MessageIds.RECOVER).sendToTarget(); } private void runRecoveryOnWorkThread() { if (!Looper.myLooper().equals(workLooper)) { throw new IllegalStateException( "Cannot run recovery on a thread other than the work" + " thread!"); } recoverPartialWrites(); recoverPartialDeletes(); } private void recoverPartialDeletes() { List pendingDeleteKeys = journal.getPendingDeleteKeys(); diskCache.delete(pendingDeleteKeys); } private void recoverPartialWrites() { File[] partialWrites = diskCacheDir.listFiles( new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.endsWith(JournaledLruDiskCache.TEMP_FILE_INDICATOR); } }); if (partialWrites != null) { for (File file : partialWrites) { diskCache.recoverPartialWrite(file); } } } private class RecoveryCallback implements Handler.Callback { @Override public boolean handleMessage(Message msg) { if (msg.what != MessageIds.RECOVER) { return false; } runRecoveryOnWorkThread(); return true; } } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/SizeJournal.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.content.ContentValues; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import android.database.sqlite.SQLiteTransactionListener; import androidx.annotation.NonNull; import androidx.core.util.Pools.Pool; import com.bumptech.glide.util.pool.FactoryPools; import com.bumptech.glide.util.pool.FactoryPools.Poolable; import com.bumptech.glide.util.pool.FactoryPools.Resetter; import com.bumptech.glide.util.pool.StateVerifier; import java.util.concurrent.atomic.AtomicLong; final class SizeJournal { private static final String UPDATE_CACHE_SIZE_SQL = "UPDATE " + SizeTable.TABLE_NAME + " SET " + SizeTable.Columns.SIZE + " = " + SizeTable.Columns.SIZE + " + ?"; private static final int UPDATE_CACHE_SIZE_SIZE_INCREMENT_IDX = 1; private static final String CONTAINS_SIZE_QUERY = "SELECT COUNT(*) FROM " + SizeTable.TABLE_NAME; private static final String CACHE_SIZE_QUERY = "SELECT " + SizeTable.Columns.SIZE + " FROM " + SizeTable.TABLE_NAME; private final AtomicLong size = new AtomicLong(); private final SqliteStatementPool updateCacheSizePool; private final Pool sizeListenerPool = FactoryPools.threadSafe( /* size= */ 20, new FactoryPools.Factory() { @Override public SizeSQLiteTransactionListener create() { return new SizeSQLiteTransactionListener(); } }, new Resetter() { @Override public void reset(@NonNull SizeSQLiteTransactionListener object) { object.clear(); } }); private final DiskCacheDbHelper dbHelper; SizeJournal(DiskCacheDbHelper dbHelper) { this.dbHelper = dbHelper; updateCacheSizePool = new SqliteStatementPool(dbHelper); } void open() { SQLiteDatabase db = dbHelper.getReadableDatabase(); boolean containsSize = 0 != DatabaseUtils.longForQuery(db, CONTAINS_SIZE_QUERY, null /*selectionArgs*/); final long currentSize; if (!containsSize) { ContentValues values = new ContentValues(); values.put(SizeTable.Columns.SIZE, 0); db.insert(SizeTable.TABLE_NAME, null /*nullColumnHack*/, values); currentSize = 0; } else { currentSize = DatabaseUtils.longForQuery(db, CACHE_SIZE_QUERY, null /*selectionArgs*/); } size.set(currentSize); } void clearInTransaction() { SQLiteDatabase db = dbHelper.getReadableDatabase(); db.delete(SizeTable.TABLE_NAME, null /*whereClause*/, null /*whereArgs*/); size.set(0); } long getCacheSizeBytes() { return size.get(); } SizeSQLiteTransactionListener prepareSizeTransaction() { SizeSQLiteTransactionListener result = sizeListenerPool.acquire(); if (result == null) { result = new SizeSQLiteTransactionListener(); } return result; } void endSizeTransaction(SizeSQLiteTransactionListener listener) { listener.clear(); sizeListenerPool.release(listener); } void decrementSizeInTransaction(SizeSQLiteTransactionListener sizeListener, long decrementBy) { incrementSizeInTransaction(sizeListener, -decrementBy); } void incrementSizeInTransaction(SizeSQLiteTransactionListener sizeListener, long incrementBy) { sizeListener.updatedSize = incrementBy; SQLiteStatement updateCacheSizeStatement = updateCacheSizePool.obtain(UPDATE_CACHE_SIZE_SQL); try { updateCacheSizeStatement.bindLong(UPDATE_CACHE_SIZE_SIZE_INCREMENT_IDX, incrementBy); updateCacheSizeStatement.executeUpdateDelete(); size.addAndGet(incrementBy); } finally { updateCacheSizePool.offer(UPDATE_CACHE_SIZE_SQL, updateCacheSizeStatement); } } /** A listener that reverts size changes upon transaction failure. */ final class SizeSQLiteTransactionListener implements SQLiteTransactionListener, Poolable { private long updatedSize; void clear() { updatedSize = 0; } @Override public void onBegin() {} @Override public void onCommit() {} @Override public void onRollback() { // Revert the increment of size on transaction failure. size.addAndGet(-updatedSize); } @NonNull @Override public StateVerifier getVerifier() { return StateVerifier.newInstance(); } } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/SizeTable.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; final class SizeTable { static final String TABLE_NAME = "size"; interface Columns { String ID = "id"; /** The total size in bytes of all files in the cache (+- pending deletes and inserts). */ String SIZE = "size"; } static String getSqlCreateStatement() { return "CREATE TABLE " + TABLE_NAME + " (" + Columns.ID + " INTEGER PRIMARY KEY, " + Columns.SIZE + " INTEGER NOT NULL DEFAULT 0" + ")"; } } ================================================ FILE: integration/sqljournaldiskcache/src/main/java/com/bumptech/glide/integration/sqljournaldiskcache/SqliteStatementPool.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; import java.util.ArrayDeque; import java.util.HashMap; import java.util.Map; import java.util.Queue; final class SqliteStatementPool { private static final int MAX_SIZE = 10; private final Map> pool = new HashMap<>(); private final SQLiteOpenHelper dbHelper; SqliteStatementPool(SQLiteOpenHelper dbHelper) { this.dbHelper = dbHelper; } SQLiteStatement obtain(String sql) { SQLiteStatement statement = null; synchronized (pool) { Queue queueForSql = pool.get(sql); if (queueForSql != null) { statement = queueForSql.poll(); } } if (statement == null) { statement = dbHelper.getWritableDatabase().compileStatement(sql); } return statement; } void offer(String sql, SQLiteStatement statement) { statement.clearBindings(); synchronized (pool) { Queue queueForSql = pool.get(sql); if (queueForSql == null) { queueForSql = new ArrayDeque<>(MAX_SIZE); pool.put(sql, queueForSql); } if (queueForSql.size() < MAX_SIZE) { queueForSql.offer(statement); } } } } ================================================ FILE: integration/sqljournaldiskcache/src/test/java/com/bumptech/glide/integration/sqljournaldiskcache/DiskCacheDbHelperUpgradeTest.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DiskCacheDbHelperUpgradeTest { private final Context context = ApplicationProvider.getApplicationContext(); @Test public void onUpgrade_fromVersionOneToTwo_producesFunctionalTablesAndColumns() throws IOException { try (DiskCacheDbHelper versionOneHelper = new DiskCacheDbHelper(context, /* isInMemory= */ false, /* databaseVersion= */ 1)) { versionOneHelper.getWritableDatabase(); } try (DiskCacheDbHelper versionTwoHelper = new DiskCacheDbHelper(context, /* isInMemory= */ false, /* databaseVersion= */ 2)) { versionTwoHelper.getWritableDatabase(); } ensureWeCanReadFromDiskCache(); } // A poor mans way of ensuring that we can read from the various sqlite tables in the way we // expect. private void ensureWeCanReadFromDiskCache() throws IOException { try (DiskCacheDbHelper diskCacheDbHelper = DiskCacheDbHelper.forProd(context)) { JournaledLruDiskCache diskCache = new JournaledLruDiskCache( context.getCacheDir(), diskCacheDbHelper, /* maximumSizeBytes= */ Long.MAX_VALUE, /* staleEvictionThresholdMs= */ Long.MAX_VALUE, new DefaultClock()); String key = "key"; File file = diskCache.beginPut(key); try { try (FileOutputStream os = new FileOutputStream(file)) { os.write(1); } diskCache.commitPut(key, file); } finally { diskCache.abortPutIfNotCommitted(key, file); } assertThat(diskCache.get(key)).isNotNull(); } } } ================================================ FILE: integration/sqljournaldiskcache/src/test/java/com/bumptech/glide/integration/sqljournaldiskcache/DiskCacheUtils.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import androidx.test.core.app.ApplicationProvider; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import org.junit.rules.ExternalResource; final class DiskCacheUtils { private DiskCacheUtils() {} static void writeToFile(File file, String data) { byte[] bytes = data.getBytes(); writeToFile(file, bytes); } static void writeToFile(File file, byte[] bytes) { try (OutputStream os = new FileOutputStream(file)) { os.write(bytes); } catch (IOException e) { throw new RuntimeException(e); } } static byte[] readFromFile(File file) { byte[] result = new byte[(int) file.length()]; try (FileInputStream is = new FileInputStream(file)) { int readSoFar = 0; int read; while ((read = is.read(result, readSoFar, result.length - readSoFar)) != -1 && readSoFar < result.length) { readSoFar += read; } if (readSoFar != result.length) { throw new IllegalStateException("Failed to read all data from: " + file); } } catch (IOException e) { throw new RuntimeException(e); } return result; } private static void deleteRecursively(File file) { if (file.isDirectory()) { File[] children = file.listFiles(); if (children != null) { for (File f : children) { deleteRecursively(f); } } } else { if (!file.delete() && file.exists()) { throw new IllegalStateException("Failed to delete; " + file); } } } static final class DiskCacheDirRule extends ExternalResource { private File cacheDir; @Override protected void before() throws Throwable { cacheDir = new File(ApplicationProvider.getApplicationContext().getCacheDir(), "test_sql_cache"); super.before(); } @Override protected void after() { super.after(); deleteRecursively(cacheDir); } void cleanup() { deleteRecursively(cacheDir); } File diskCacheDir() { return cacheDir; } } } ================================================ FILE: integration/sqljournaldiskcache/src/test/java/com/bumptech/glide/integration/sqljournaldiskcache/JournaledLruDiskCacheTest.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.content.Context; import android.os.Looper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.integration.sqljournaldiskcache.DiskCacheUtils.DiskCacheDirRule; import com.bumptech.glide.util.Preconditions; import java.io.File; import java.io.IOException; import java.time.Duration; import java.util.Collections; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class JournaledLruDiskCacheTest { private final Context context = ApplicationProvider.getApplicationContext(); @Rule public final DiskCacheDirRule diskCacheDirRule = new DiskCacheDirRule(); private final TestClock testClock = new TestClock(); private JournaledLruDiskCache cache; private int size; private FileSystem fileSystem; private DiskCacheDbHelper dbHelper; private File cacheDir; @Before public void setUp() { dbHelper = DiskCacheDbHelper.forTesting(context); cacheDir = diskCacheDirRule.diskCacheDir(); fileSystem = spy(new FileSystem() {}); size = 1024; cache = newCache(); } private JournaledLruDiskCache newCache() { return newCache(/* evictionSlopMultiplier= */ 0f); } private JournaledLruDiskCache newCache(float evictionSlopMultiplier) { return new JournaledLruDiskCache( cacheDir, dbHelper, fileSystem, size, Looper.getMainLooper(), evictionSlopMultiplier, /* updateModifiedTimeBatchSize= */ 1, /* staleEvictionThresholdMs= */ Long.MAX_VALUE, testClock::currentTimeMillis); } @After public void tearDown() { dbHelper.close(); } @Test public void beginPut_createsCanaryFile() { cache.beginPut("key"); assertThat(cacheDir.listFiles()).hasLength(1); } @Test public void beginPut_withExistingFileForKey_returnsNull() { String key = "key"; File file = cache.beginPut(key); try { DiskCacheUtils.writeToFile(file, "data"); cache.commitPut(key, file); } finally { cache.abortPutIfNotCommitted(key, file); } File secondPutFile = cache.beginPut(key); assertThat(secondPutFile).isNull(); } @Test public void commitPut_withFailedPreviousWrite_leavesSizeConsistent() { String key = "key"; File temp = cache.beginPut(key); try { when(fileSystem.rename(temp, new File(cacheDir, key))).thenReturn(false).thenCallRealMethod(); // Write a file so large it should get evicted immediately byte[] bytes = new byte[size * 2]; DiskCacheUtils.writeToFile(temp, bytes); cache.commitPut(key, temp); } finally { cache.abortPutIfNotCommitted(key, temp); } // Verify file was evicted and size is 0 since the big file was evicted. assertThat(getSize(cacheDir)).isEqualTo(0); assertThat(cache.getCurrentSizeBytes()).isEqualTo(0); } @Test public void commitPut_withFailedPreviousWrite_replacesContent() { String key = "key"; File temp = cache.beginPut(key); // This is a spy, rename below actually performs the rename (which just renames nothing to // nothing in this case), it must come before writeToFile or commitPut. when(fileSystem.rename(temp, new File(cacheDir, key))).thenReturn(false).thenCallRealMethod(); DiskCacheUtils.writeToFile(temp, "first data"); cache.commitPut(key, temp); // If the app crashes prior to abortIfNotCommitted: cache = newCache(); String expectedData = "second data"; temp = cache.beginPut(key); try { DiskCacheUtils.writeToFile(temp, expectedData); cache.commitPut(key, temp); } finally { cache.abortPutIfNotCommitted(key, temp); } assertThat(cache.get(key)).isNotNull(); assertThat(readFromFile(cache.get(key))).isEqualTo(expectedData); } @Test public void testAbortPutIfNotCommitted_handlesNullFiles() { String key = "key"; cache.beginPut(key); cache.abortPutIfNotCommitted(key, null); } @Test public void abortPutIfNotCommitted_decrementsSizeIfRenameToFails() { // Write a large File and then fail to rename it so the journal size temporarily doesn't match // the file system size. The slop multiplier will then cause the cache to calculate an amount // to delete that is more than the number of Files available, unless we've properly accounted // for the rename failure. cache = newCache(/* evictionSlopMultiplier= */ 0.5f); String largeKey = "large"; File file = cache.beginPut(largeKey); try { // This is a spy, rename below actually performs the rename (which just renames nothing to // nothing in this case), it must come before writeToFile or commitPut. when(fileSystem.rename(file, new File(cacheDir, largeKey))).thenReturn(false); byte[] bytes = new byte[size - 1]; DiskCacheUtils.writeToFile(file, bytes); cache.commitPut(largeKey, file); } finally { cache.abortPutIfNotCommitted(largeKey, file); } String smallKey = "key"; int totalSmallFiles = 2; for (int i = 0; i < totalSmallFiles; i++) { String key = smallKey + i; File smallFile = cache.beginPut(key); try { byte[] bytes = new byte[(size / totalSmallFiles) - 1]; DiskCacheUtils.writeToFile(smallFile, bytes); cache.commitPut(key, smallFile); } finally { cache.abortPutIfNotCommitted(key, smallFile); } } for (int i = 0; i < totalSmallFiles; i++) { assertThat(cache.get(smallKey + i)).isNotNull(); } } @Test public void abortPutIfNotCommitted_decrementsSizeInJournalIfRenameToFails() { cache = newCache(/* evictionSlopMultiplier= */ 0.5f); String largeKey = "large"; File file = cache.beginPut(largeKey); try { // This is a spy, rename below actually performs the rename (which just renames nothing to // nothing in this case), it must come before writeToFile or commitPut. when(fileSystem.rename(file, new File(cacheDir, largeKey))).thenReturn(false); byte[] bytes = new byte[size - 1]; DiskCacheUtils.writeToFile(file, bytes); cache.commitPut(largeKey, file); } finally { cache.abortPutIfNotCommitted(largeKey, file); } // Re-open the cache. cache = newCache(/* evictionSlopMultiplier= */ 0.5f); String smallKey = "key"; int totalSmallFiles = 2; for (int i = 0; i < totalSmallFiles; i++) { String key = smallKey + i; File smallFile = cache.beginPut(key); try { byte[] bytes = new byte[(size / totalSmallFiles) - 1]; DiskCacheUtils.writeToFile(smallFile, bytes); cache.commitPut(key, smallFile); } finally { cache.abortPutIfNotCommitted(key, smallFile); } } for (int i = 0; i < totalSmallFiles; i++) { assertThat(cache.get(smallKey + i)).isNotNull(); } } @Test(expected = IllegalMonitorStateException.class) public void testAbortPutIfNotCommitted_withoutBeginPut_throws() { cache.abortPutIfNotCommitted("fakeKey", new File(cacheDir, "fakeFile")); } @Test public void get_afterCommittedPut_returnsFileWithData() { String key = "myKey"; String data = "data"; File toPut = cache.beginPut(key); try { DiskCacheUtils.writeToFile(toPut, data); cache.commitPut(key, toPut); } finally { cache.abortPutIfNotCommitted(key, toPut); } File fromGet = cache.get(key); assertThat(readFromFile(fromGet)).isEqualTo(data); } @Test public void get_beforePut_returnsNull() { assertThat(cache.get("key")).isNull(); } @Test public void get_afterAbortedPut_returnsNull() { String key = "key"; File toPut = cache.beginPut(key); try { String data = "data"; DiskCacheUtils.writeToFile(toPut, data); } finally { cache.abortPutIfNotCommitted(key, toPut); } File fromGet = cache.get(key); assertThat(fromGet).isNull(); } @Test public void abortPutIfNotCommitted_whenNotCommitted_discardsData() { String key = "key"; File toPut = cache.beginPut(key); try { String data = "data"; DiskCacheUtils.writeToFile(toPut, data); } finally { cache.abortPutIfNotCommitted(key, toPut); } assertThat(cacheDir.listFiles()).hasLength(1); assertThat(getSize(cacheDir)).isEqualTo(0L); } @Test public void commitPut_runsEvictionIfNecessary() { int totalFiles = 5; byte[] data = new byte[size / 3]; for (int i = 0; i < totalFiles; i++) { String key = "key" + i; File file = cache.beginPut(key); try { DiskCacheUtils.writeToFile(file, data); cache.commitPut(key, file); } finally { cache.abortPutIfNotCommitted(key, file); } } onIdleWorkerThread(); assertThat(getSize(cacheDir)).isLessThan((long) size); } @Test public void eviction_removesFirstPutFile() { int totalFiles = 3; byte[] data = new byte[(size / totalFiles) + 1]; String keyBase = "key"; for (int i = 0; i < totalFiles; i++) { String key = keyBase + i; File file = cache.beginPut(key); try { DiskCacheUtils.writeToFile(file, data); cache.commitPut(key, file); } finally { cache.abortPutIfNotCommitted(key, file); } testClock.advance(Duration.ofMillis(1)); } onIdleWorkerThread(); assertThat(cache.get(keyBase + 0)).isNull(); assertThat(cache.get(keyBase + 1)).isNotNull(); assertThat(cache.get(keyBase + 2)).isNotNull(); } // Eviction is triggered by posts. private static void onIdleWorkerThread() { shadowOf(Looper.getMainLooper()).idle(); } @Test public void eviction_withGets_removesLeastRecentlyUsedFile() { int totalFiles = 3; byte[] data = new byte[(size / totalFiles) + 1]; String keyBase = "key"; for (int i = 0; i < totalFiles; i++) { String key = keyBase + i; File file = cache.beginPut(key); try { DiskCacheUtils.writeToFile(file, data); cache.commitPut(key, file); } finally { cache.abortPutIfNotCommitted(key, file); } if (i == 1) { testClock.advance(Duration.ofMillis(1)); cache.get(keyBase + 0); } testClock.advance(Duration.ofMillis(1)); } onIdleWorkerThread(); assertThat(cache.get(keyBase + 0)).isNotNull(); assertThat(cache.get(keyBase + 1)).isNull(); assertThat(cache.get(keyBase + 2)).isNotNull(); } @Test public void eviction_withManyEntries_updatesSizeCorrectly() { int numSmallFiles = 3; byte[] largeData = new byte[size - 1]; byte[] smallData = new byte[(size / numSmallFiles) - 1]; String largeKey = "largeKey"; for (int i = 0; i < 2; i++) { String key = largeKey + i; File largeFile = cache.beginPut(key); try { DiskCacheUtils.writeToFile(largeFile, largeData); cache.commitPut(key, largeFile); } finally { cache.abortPutIfNotCommitted(key, largeFile); } testClock.advance(Duration.ofMillis(1)); } String smallkey = "smallKey"; for (int i = 0; i < numSmallFiles; i++) { String key = smallkey + i; File file = cache.beginPut(key); try { DiskCacheUtils.writeToFile(file, smallData); cache.commitPut(key, file); } finally { cache.abortPutIfNotCommitted(key, file); } testClock.advance(Duration.ofMillis(1)); } onIdleWorkerThread(); for (int i = 0; i < numSmallFiles; i++) { assertThat(cache.get(smallkey + i)).isNotNull(); } } // The goal here is to ensure our sql batching works as expected. We aim for more than 999 files // because sql only allows 999 arguments for a single query. @Test public void eviction_writeManyFiles_evictsManyEntries() throws IOException { String smallKey = "small"; for (int i = 0; i < 1000; i++) { String key = smallKey + i; File file = cache.beginPut(key); try { if (!file.createNewFile()) { throw new IllegalStateException("Failed to create: " + file); } cache.commitPut(key, file); } finally { cache.abortPutIfNotCommitted(key, file); } testClock.advance(Duration.ofMillis(1)); } String largeKey = "large"; File largeFile = cache.beginPut(largeKey); try { byte[] bytes = new byte[size + 1]; DiskCacheUtils.writeToFile(largeFile, bytes); cache.commitPut(largeKey, largeFile); } finally { cache.abortPutIfNotCommitted(largeKey, largeFile); } onIdleWorkerThread(); assertThat(cacheDir.listFiles()).hasLength(1); } @Test public void delete_missingFile_ignored() { cache.delete("fakeKey"); } @Test public void delete_removesEntryForKey() { String key = "key"; File temp = cache.beginPut(key); try { DiskCacheUtils.writeToFile(temp, "data"); cache.commitPut(key, temp); } finally { cache.abortPutIfNotCommitted(key, temp); } assertThat(cache.get(key)).isNotNull(); assertThat(cacheDir.listFiles()).hasLength(2); cache.delete(key); assertThat(cache.get(key)).isNull(); assertThat(cacheDir.listFiles()).hasLength(1); } @Test public void delete_withInProgressWriteForKey_doesNotDeleteKey() { String key = "key"; File file = new File(cacheDir, key); when(fileSystem.delete(file)).thenReturn(false); when(fileSystem.exists(file)).thenReturn(false).thenReturn(true); assertThat(cache.delete(Collections.singletonList(key))).isEmpty(); } @Test public void delete_onPreviouslyFailedKey_doesNotDecrementCacheSizeTwice() { String key = "key"; File file = new File(cacheDir, key); // first delete attempt, second delete attempt. when(fileSystem.delete(file)).thenReturn(false).thenCallRealMethod(); File temp = cache.beginPut(key); try { byte[] bytes = new byte[size - 1]; DiskCacheUtils.writeToFile(temp, bytes); cache.commitPut(key, temp); } finally { cache.abortPutIfNotCommitted(key, temp); } cache.delete(key); cache.delete(key); // We should have successfully deleted the file. assertThat(cacheDir.listFiles()).hasLength(1); String otherKey = "other"; for (int i = 0; i < 2; i++) { String currentKey = otherKey + i; temp = cache.beginPut(currentKey); try { byte[] bytes = new byte[size - 1]; DiskCacheUtils.writeToFile(temp, bytes); cache.commitPut(currentKey, temp); } finally { cache.abortPutIfNotCommitted(currentKey, file); } } onIdleWorkerThread(); // Only one File should remain. Two will if we double counted the delete of the single key. assertThat(cacheDir.listFiles()).hasLength(2); } @Test public void clear_removesAllEntriesAndFiles() { String firstKey = "key1"; File temp = cache.beginPut(firstKey); try { DiskCacheUtils.writeToFile(temp, "data1"); cache.commitPut(firstKey, temp); } finally { cache.abortPutIfNotCommitted(firstKey, temp); } testClock.advance(Duration.ofMillis(1)); String secondKey = "key2"; temp = cache.beginPut(secondKey); try { DiskCacheUtils.writeToFile(temp, secondKey); cache.commitPut(secondKey, temp); } finally { cache.abortPutIfNotCommitted(secondKey, temp); } assertThat(cache.get(firstKey)).isNotNull(); assertThat(cache.get(secondKey)).isNotNull(); assertThat(cacheDir.listFiles()).hasLength(3); cache.clear(); assertThat(cache.get(firstKey)).isNull(); assertThat(cache.get(secondKey)).isNull(); // Now it should just contain the canary. assertThat(cacheDir.listFiles()).hasLength(1); } @Test public void recovery_withPartialWriteAndJournalEntry_deletesTempFileAndDecrementsSize() { String successKey = "success"; File successTemp = cache.beginPut(successKey); try { byte[] bytes = new byte[size / 2]; DiskCacheUtils.writeToFile(successTemp, bytes); cache.commitPut(successKey, successTemp); } finally { cache.abortPutIfNotCommitted(successKey, successTemp); } onIdleWorkerThread(); Preconditions.checkNotNull(cache.get(successKey)); String failKey = "fail"; File failPermanent = new File(cacheDir, failKey); File failTemp = cache.beginPut(failKey); when(fileSystem.rename(failTemp, failPermanent)).thenReturn(false).thenCallRealMethod(); // Simulate a crash by failing to calll abortPutIfNotCommitted. byte[] bytes1 = new byte[(size / 2) - 1]; DiskCacheUtils.writeToFile(failTemp, bytes1); cache.commitPut(failKey, failTemp); // We should have the success permanent file, the failed temp file, and the canary file. assertThat(cacheDir.listFiles()).hasLength(3); // Re-open the cache. cache = newCache(); String secondSuccessKey = "secondSuccess"; File secondSuccessTemp = cache.beginPut(secondSuccessKey); try { byte[] bytes = new byte[(size / 2) - 1]; DiskCacheUtils.writeToFile(secondSuccessTemp, bytes); cache.commitPut(secondSuccessKey, secondSuccessTemp); } finally { cache.abortPutIfNotCommitted(secondSuccessKey, secondSuccessTemp); } onIdleWorkerThread(); assertThat(cache.get(successKey)).isNotNull(); assertThat(cache.get(failKey)).isNull(); assertThat(cache.get(secondSuccessKey)).isNotNull(); } @Test public void recovery_withPartialWriteAndNoJournalEntry_deletesTempFile() { String partialWriteKey = "partialWriteKey"; File partialWriteTemp = cache.beginPut(partialWriteKey); byte[] bytes1 = new byte[size]; DiskCacheUtils.writeToFile(partialWriteTemp, bytes1); cache = newCache(); // Verify we haven't done unexpected things to the cache size. String baseKey = "key"; for (int i = 0; i < 4; i++) { String key = baseKey + i; File temp = cache.beginPut(key); try { byte[] bytes = new byte[(size / 4) + 1]; DiskCacheUtils.writeToFile(temp, bytes); cache.commitPut(key, temp); } finally { cache.abortPutIfNotCommitted(key, temp); } testClock.advance(Duration.ofMillis(1)); } onIdleWorkerThread(); // Canary + 3 smaller files. assertThat(cacheDir.listFiles()).hasLength(4); for (int i = 0; i < 4; i++) { String key = baseKey + i; if (i == 0) { assertThat(cache.get(key)).isNull(); } else { assertThat(cache.get(key)).isNotNull(); } } } @Test public void recovery_withPendingDeleteForFile_deletesFileAndEntry() { String key = "key"; File permanentFile = new File(cacheDir, key); when(fileSystem.delete(permanentFile)).thenReturn(false).thenCallRealMethod(); File temp = cache.beginPut(key); try { DiskCacheUtils.writeToFile(temp, "data"); cache.commitPut(key, temp); } finally { cache.abortPutIfNotCommitted(key, temp); } // Failed delete. cache.delete(key); // Failed delete + canary. assertThat(cacheDir.listFiles()).hasLength(2); cache = newCache(); String otherKey = "other"; temp = cache.beginPut(otherKey); try { DiskCacheUtils.writeToFile(temp, "otherData"); cache.commitPut(otherKey, temp); } finally { cache.abortPutIfNotCommitted(otherKey, temp); } onIdleWorkerThread(); assertThat(cache.get(key)).isNull(); // Canary + second key. assertThat(cacheDir.listFiles()).hasLength(2); } @Test public void recovery_withInProgressWrite_doesNotDeleteFile() { String key = "key"; String data = "data"; File temp = cache.beginPut(key); try { DiskCacheUtils.writeToFile(temp, data); cache.commitPut(key, temp); } finally { cache.abortPutIfNotCommitted(key, temp); } // Simulate a concurrent recovery attempt now obtaining the write lock. cache.recoverPartialWrite(temp); // Make sure that it doesn't delete the fully written file File cacheFile = cache.get(key); assertThat(cacheFile).isNotNull(); assertThat(readFromFile(cacheFile)).isEqualTo(data); } @Test public void setMaximumSizeBytes_increaseCacheSize_doesNotEvictEntries() { String key = "key"; File toPut = cache.beginPut(key); cache.setMaximumSizeBytes(size * 3); try { // write a file that exceeds the old maximum byte[] bytes = new byte[size * 3]; DiskCacheUtils.writeToFile(toPut, bytes); cache.commitPut(key, toPut); } finally { cache.abortPutIfNotCommitted(key, toPut); } assertThat(getSize(cacheDir)).isEqualTo(size * 3); assertThat(cache.getCurrentSizeBytes()).isEqualTo(size * 3); } @Test public void setMaximumSizeBytes_increaseCacheSize_evictEntries() { String key = "key"; File toPut = cache.beginPut(key); cache.setMaximumSizeBytes(size * 2); try { // write a file that exceeds the new max byte[] bytes = new byte[size * 3]; DiskCacheUtils.writeToFile(toPut, bytes); cache.commitPut(key, toPut); } finally { cache.abortPutIfNotCommitted(key, toPut); } onIdleWorkerThread(); assertThat(getSize(cacheDir)).isEqualTo(0); assertThat(cache.getCurrentSizeBytes()).isEqualTo(0); } @Test public void setMaximumSizeBytes_decreaseCacheSize_doesNotEvictEntries() { String key = "key"; File toPut = cache.beginPut(key); int tinySize = 20; try { // write a file that satisfies original and new cache space byte[] bytes = new byte[tinySize]; DiskCacheUtils.writeToFile(toPut, bytes); cache.commitPut(key, toPut); } finally { cache.abortPutIfNotCommitted(key, toPut); } onIdleWorkerThread(); assertThat(getSize(cacheDir)).isEqualTo(tinySize); assertThat(cache.getCurrentSizeBytes()).isEqualTo(tinySize); // shrinking size should not evict int newMax = size - 100; assertThat(newMax).isLessThan(size); cache.setMaximumSizeBytes(newMax); onIdleWorkerThread(); assertThat(getSize(cacheDir)).isAtMost(tinySize); assertThat(cache.getCurrentSizeBytes()).isAtMost(tinySize); } @Test public void setMaximumSizeBytes_decreaseCacheSize_evictEntries() { String key = "key"; File toPut = cache.beginPut(key); try { // write a file that satisfies original cache space byte[] bytes = new byte[size]; DiskCacheUtils.writeToFile(toPut, bytes); cache.commitPut(key, toPut); } finally { cache.abortPutIfNotCommitted(key, toPut); } onIdleWorkerThread(); assertThat(getSize(cacheDir)).isEqualTo(size); assertThat(cache.getCurrentSizeBytes()).isEqualTo(size); // shrinking size should evict cache as needed int newMax = size - 100; assertThat(newMax).isLessThan(size); cache.setMaximumSizeBytes(newMax); onIdleWorkerThread(); assertThat(getSize(cacheDir)).isAtMost(newMax); assertThat(cache.getCurrentSizeBytes()).isAtMost(newMax); } @Test public void setMaximumSizeBytes_decreaseCacheSize_evictStaleEntries() { String keyStale = "keyStale"; String keyLru = "keyLru"; File toPutStale = cache.beginPut(keyStale); File toPutLru = cache.beginPut(keyLru); int smallSizeBytes = 1; try { byte[] bytes = new byte[size - smallSizeBytes]; DiskCacheUtils.writeToFile(toPutStale, bytes); cache.commitPut(keyStale, toPutStale); } finally { cache.abortPutIfNotCommitted(keyStale, toPutStale); } // make the next entry far ahead in the future testClock.set(90); try { byte[] bytes = new byte[smallSizeBytes]; DiskCacheUtils.writeToFile(toPutLru, bytes); cache.commitPut(keyLru, toPutLru); } finally { cache.abortPutIfNotCommitted(keyLru, toPutLru); } onIdleWorkerThread(); assertThat(getSize(cacheDir)).isEqualTo(size); assertThat(cache.getCurrentSizeBytes()).isEqualTo(size); // shrinking size should evict cache as needed int newMax = size - 100; assertThat(newMax).isLessThan(size); assertThat(smallSizeBytes).isLessThan(newMax); cache.setMaximumSizeBytes(newMax); onIdleWorkerThread(); assertThat(getSize(cacheDir)).isAtMost(smallSizeBytes); assertThat(cache.getCurrentSizeBytes()).isAtMost(smallSizeBytes); } @Test public void setMaximumSizeBytes_decreaseCacheSize_evictLruEntries() { String keyStale = "keyStale"; String keyLru = "keyLru"; File toPutStale = cache.beginPut(keyStale); File toPutLru = cache.beginPut(keyLru); int smallSizeBytes = 1; try { byte[] bytes = new byte[smallSizeBytes]; DiskCacheUtils.writeToFile(toPutStale, bytes); cache.commitPut(keyStale, toPutStale); } finally { cache.abortPutIfNotCommitted(keyStale, toPutStale); } // make the next entry far ahead in the future testClock.set(90); try { byte[] bytes = new byte[size - smallSizeBytes]; DiskCacheUtils.writeToFile(toPutLru, bytes); cache.commitPut(keyLru, toPutLru); } finally { cache.abortPutIfNotCommitted(keyLru, toPutLru); } onIdleWorkerThread(); assertThat(getSize(cacheDir)).isEqualTo(size); assertThat(cache.getCurrentSizeBytes()).isEqualTo(size); // shrinking size should evict cache as needed int newMax = size - 100; assertThat(newMax).isLessThan(size); assertThat(smallSizeBytes).isLessThan(newMax); cache.setMaximumSizeBytes(newMax); onIdleWorkerThread(); assertThat(getSize(cacheDir)).isAtMost(smallSizeBytes); assertThat(cache.getCurrentSizeBytes()).isAtMost(smallSizeBytes); } private static long getSize(File file) { long result = 0; if (file.isDirectory()) { for (File f : file.listFiles()) { result += getSize(f); } } else { result = file.length(); } return result; } private static String readFromFile(File file) { byte[] data = DiskCacheUtils.readFromFile(file); return new String(data); } } ================================================ FILE: integration/sqljournaldiskcache/src/test/java/com/bumptech/glide/integration/sqljournaldiskcache/TestClock.java ================================================ package com.bumptech.glide.integration.sqljournaldiskcache; import java.time.Duration; final class TestClock implements Clock { private long currentTimeMillis = 0L; @Override public long currentTimeMillis() { return currentTimeMillis; } void set(long timeMillis) { currentTimeMillis = timeMillis; } void advance(Duration duration) { currentTimeMillis += duration.toMillis(); } } ================================================ FILE: integration/volley/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.integration.volley" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) api(libs.volley) api(libs.androidx.annotation) annotationProcessor(project(":annotation:compiler")) testImplementation(project(":testutil")) testImplementation(libs.truth) testImplementation(libs.junit) testImplementation(libs.mockito.core) testImplementation(libs.robolectric) testImplementation(libs.mockwebserver) testImplementation(libs.androidx.test.core) testImplementation(libs.androidx.junit) testImplementation(libs.androidx.test.runner) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: integration/volley/gradle.properties ================================================ POM_NAME=Glide Volley Integration POM_ARTIFACT_ID=volley-integration POM_PACKAGING=aar POM_DESCRIPTION=An integration library to use Volley to fetch data over http/https in Glide ================================================ FILE: integration/volley/lint.xml ================================================ ================================================ FILE: integration/volley/src/main/AndroidManifest.xml ================================================ ================================================ FILE: integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyGlideModule.java ================================================ package com.bumptech.glide.integration.volley; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import com.bumptech.glide.load.model.GlideUrl; import java.io.InputStream; /** * A {@link com.bumptech.glide.module.GlideModule} implementation to replace Glide's default {@link * java.net.HttpURLConnection} based {@link com.bumptech.glide.load.model.ModelLoader} with a Volley * based {@link com.bumptech.glide.load.model.ModelLoader}. * *

    If you're using gradle, you can include this module simply by depending on the aar, the module * will be merged in by manifest merger. For other build systems or for more more information, see * {@link com.bumptech.glide.module.GlideModule}. * * @deprecated Replaced with {@link VolleyLibraryGlideModule}. */ @Deprecated @SuppressWarnings("deprecation") public class VolleyGlideModule implements com.bumptech.glide.module.GlideModule { @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { // Do nothing. } @Override public void registerComponents(Context context, Glide glide, Registry registry) { registry.replace(GlideUrl.class, InputStream.class, new VolleyUrlLoader.Factory(context)); } } ================================================ FILE: integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyLibraryGlideModule.java ================================================ package com.bumptech.glide.integration.volley; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.LibraryGlideModule; import java.io.InputStream; /** * A {@link com.bumptech.glide.module.GlideModule} implementation to replace Glide's default {@link * java.net.HttpURLConnection} based {@link com.bumptech.glide.load.model.ModelLoader} with a Volley * based {@link com.bumptech.glide.load.model.ModelLoader}. * *

    For Applications that depend on this library and include an {@link AppGlideModule} and Glide's * annotation processor, this class will be automatically included. */ @GlideModule public class VolleyLibraryGlideModule extends LibraryGlideModule { @Override public void registerComponents( @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { registry.replace(GlideUrl.class, InputStream.class, new VolleyUrlLoader.Factory(context)); } } ================================================ FILE: integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyRequestFactory.java ================================================ package com.bumptech.glide.integration.volley; import com.android.volley.Request; import com.android.volley.Request.Priority; import com.bumptech.glide.load.data.DataFetcher.DataCallback; import java.io.InputStream; import java.util.Map; /** Used to construct a custom Volley request, such as for authentication header decoration. */ public interface VolleyRequestFactory { /** * Returns a Volley request for the given image url. The given future should be put as a listener * or called when the request completes. */ Request create( String url, DataCallback callback, Priority priority, Map headers); } ================================================ FILE: integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyStreamFetcher.java ================================================ package com.bumptech.glide.integration.volley; import android.util.Log; import androidx.annotation.NonNull; import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.HttpHeaderParser; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Collections; import java.util.Map; /** A DataFetcher backed by volley for fetching images via http. */ // Public API. @SuppressWarnings("WeakerAccess") public class VolleyStreamFetcher implements DataFetcher { private static final String TAG = "VolleyStreamFetcher"; public static final VolleyRequestFactory DEFAULT_REQUEST_FACTORY = new VolleyRequestFactory() { @Override public Request create( String url, DataCallback callback, Request.Priority priority, Map headers) { return new GlideRequest(url, callback, priority, headers); } }; private final RequestQueue requestQueue; private final VolleyRequestFactory requestFactory; private final GlideUrl url; private volatile Request request; @SuppressWarnings("unused") public VolleyStreamFetcher(RequestQueue requestQueue, GlideUrl url) { this(requestQueue, url, DEFAULT_REQUEST_FACTORY); } public VolleyStreamFetcher( RequestQueue requestQueue, GlideUrl url, VolleyRequestFactory requestFactory) { this.requestQueue = requestQueue; this.url = url; this.requestFactory = requestFactory; } @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { request = requestFactory.create( url.toStringUrl(), callback, glideToVolleyPriority(priority), url.getHeaders()); requestQueue.add(request); } @Override public void cleanup() { // Do nothing. } @Override public void cancel() { Request local = request; if (local != null) { local.cancel(); } } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.REMOTE; } private static Request.Priority glideToVolleyPriority(@NonNull Priority priority) { switch (priority) { case LOW: return Request.Priority.LOW; case HIGH: return Request.Priority.HIGH; case IMMEDIATE: return Request.Priority.IMMEDIATE; default: return Request.Priority.NORMAL; } } /** * Default {@link com.android.volley.Request} implementation for Glide that receives errors and * results on volley's background thread. */ // Public API. @SuppressWarnings("unused") public static class GlideRequest extends Request { private final DataCallback callback; private final Priority priority; private final Map headers; public GlideRequest(String url, DataCallback callback, Priority priority) { this(url, callback, priority, Collections.emptyMap()); } public GlideRequest( String url, DataCallback callback, Priority priority, Map headers) { super(Method.GET, url, null); this.callback = callback; this.priority = priority; this.headers = headers; } @Override public Map getHeaders() { return headers; } @Override public Priority getPriority() { return priority; } @Override protected VolleyError parseNetworkError(VolleyError volleyError) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Volley failed to retrieve response", volleyError); } if (!isCanceled()) { callback.onLoadFailed(volleyError); } return super.parseNetworkError(volleyError); } @Override protected Response parseNetworkResponse(NetworkResponse response) { if (!isCanceled()) { callback.onDataReady(new ByteArrayInputStream(response.data)); } return Response.success(response.data, HttpHeaderParser.parseCacheHeaders(response)); } @Override protected void deliverResponse(byte[] response) { // Do nothing. } } } ================================================ FILE: integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyUrlLoader.java ================================================ package com.bumptech.glide.integration.volley; import android.content.Context; import androidx.annotation.NonNull; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import java.io.InputStream; /** A simple model loader for fetching media over http/https using Volley. */ public class VolleyUrlLoader implements ModelLoader { private final RequestQueue requestQueue; private final VolleyRequestFactory requestFactory; // Public API. @SuppressWarnings("unused") public VolleyUrlLoader(RequestQueue requestQueue) { this(requestQueue, VolleyStreamFetcher.DEFAULT_REQUEST_FACTORY); } // Public API. @SuppressWarnings("WeakerAccess") public VolleyUrlLoader(RequestQueue requestQueue, VolleyRequestFactory requestFactory) { this.requestQueue = requestQueue; this.requestFactory = requestFactory; } @Override public boolean handles(@NonNull GlideUrl url) { return true; } @Override public LoadData buildLoadData( @NonNull GlideUrl url, int width, int height, @NonNull Options options) { return new LoadData<>(url, new VolleyStreamFetcher(requestQueue, url, requestFactory)); } /** The default factory for {@link VolleyUrlLoader}s. */ // Public API. @SuppressWarnings("WeakerAccess") public static class Factory implements ModelLoaderFactory { private static volatile RequestQueue internalQueue; private final VolleyRequestFactory requestFactory; private final RequestQueue requestQueue; /** Constructor for a new Factory that runs requests using a static singleton request queue. */ public Factory(Context context) { this(getInternalQueue(context)); } /** Constructor for a new Factory that runs requests using the given {@link RequestQueue}. */ public Factory(RequestQueue requestQueue) { this(requestQueue, VolleyStreamFetcher.DEFAULT_REQUEST_FACTORY); } /** * Constructor for a new Factory with a custom Volley request factory that runs requests using * the given {@link RequestQueue}. */ public Factory(RequestQueue requestQueue, VolleyRequestFactory requestFactory) { this.requestFactory = requestFactory; this.requestQueue = requestQueue; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory factory) { return new VolleyUrlLoader(requestQueue, requestFactory); } @Override public void teardown() { // Do nothing. } private static RequestQueue getInternalQueue(Context context) { if (internalQueue == null) { synchronized (Factory.class) { if (internalQueue == null) { internalQueue = Volley.newRequestQueue(context); } } } return internalQueue; } } } ================================================ FILE: integration/volley/src/test/java/com/bumptech/glide/integration/volley/VolleyStreamFetcherServerTest.java ================================================ package com.bumptech.glide.integration.volley; import static com.bumptech.glide.testutil.TestUtil.assertStreamOf; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.SystemClock; import androidx.test.core.app.ApplicationProvider; import com.android.volley.RequestQueue; import com.android.volley.VolleyError; import com.android.volley.toolbox.Volley; import com.bumptech.glide.Priority; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.Headers; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowSystemClock; /** * Tests {@link com.bumptech.glide.integration.volley.VolleyStreamFetcher} against server responses. */ @RunWith(RobolectricTestRunner.class) @Config( manifest = Config.NONE, sdk = 19, shadows = VolleyStreamFetcherServerTest.FakeSystemClock.class) public class VolleyStreamFetcherServerTest { private static final String DEFAULT_PATH = "/fakepath"; @Mock private DataFetcher.DataCallback callback; private MockWebServer mockWebServer; private RequestQueue requestQueue; private ArgumentCaptor streamCaptor; private CountDownLatch waitForResponseLatch; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); waitForResponseLatch = new CountDownLatch(1); doAnswer(new CountDown()).when(callback).onDataReady(any(InputStream.class)); doAnswer(new CountDown()).when(callback).onLoadFailed(any(Exception.class)); requestQueue = Volley.newRequestQueue(ApplicationProvider.getApplicationContext()); mockWebServer = new MockWebServer(); mockWebServer.start(); streamCaptor = ArgumentCaptor.forClass(InputStream.class); } @After public void tearDown() throws IOException { mockWebServer.shutdown(); requestQueue.stop(); } @Test public void testReturnsInputStreamOnStatusOk() throws Exception { String expected = "fakedata"; mockWebServer.enqueue(new MockResponse().setBody(expected).setResponseCode(200)); DataFetcher fetcher = getFetcher(); fetcher.loadData(Priority.HIGH, callback); waitForResponseLatch.await(); verify(callback).onDataReady(streamCaptor.capture()); assertStreamOf(expected, streamCaptor.getValue()); } @Test public void testHandlesRedirect301s() throws Exception { String expected = "fakedata"; mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url("/redirect").toString())); mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected)); getFetcher().loadData(Priority.LOW, callback); waitForResponseLatch.await(); verify(callback).onDataReady(streamCaptor.capture()); assertStreamOf(expected, streamCaptor.getValue()); } @Test public void testHandlesRedirect302s() throws Exception { String expected = "fakedata"; mockWebServer.enqueue( new MockResponse() .setResponseCode(302) .setHeader("Location", mockWebServer.url("/redirect").toString())); mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected)); getFetcher().loadData(Priority.LOW, callback); waitForResponseLatch.await(); verify(callback).onDataReady(streamCaptor.capture()); assertStreamOf(expected, streamCaptor.getValue()); } @Test public void testHandlesUpToFiveRedirects() throws Exception { int numRedirects = 4; String expected = "redirectedData"; String redirectBase = "/redirect"; for (int i = 0; i < numRedirects; i++) { mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url(redirectBase + i).toString())); } mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected)); getFetcher().loadData(Priority.NORMAL, callback); waitForResponseLatch.await(); verify(callback).onDataReady(streamCaptor.capture()); assertStreamOf(expected, streamCaptor.getValue()); assertThat(mockWebServer.takeRequest().getPath()).contains(DEFAULT_PATH); for (int i = 0; i < numRedirects; i++) { assertThat(mockWebServer.takeRequest().getPath()).contains(redirectBase + i); } } @Test public void testCallsLoadFailedIfRedirectLocationIsEmpty() throws Exception { for (int i = 0; i < 2; i++) { mockWebServer.enqueue(new MockResponse().setResponseCode(301)); } getFetcher().loadData(Priority.NORMAL, callback); waitForResponseLatch.await(); verify(callback).onLoadFailed(isA(VolleyError.class)); } @Test public void testCallsLoadFailedIfStatusCodeIsNegativeOne() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(-1)); getFetcher().loadData(Priority.LOW, callback); waitForResponseLatch.await(); verify(callback).onLoadFailed(isA(VolleyError.class)); } @Test public void testCallsLoadFailedAfterTooManyRedirects() throws Exception { for (int i = 0; i < 20; i++) { mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url("/redirect" + i).toString())); } getFetcher().loadData(Priority.NORMAL, callback); waitForResponseLatch.await(); verify(callback).onLoadFailed(isA(VolleyError.class)); } @Test public void testCallsLoadFailedIfStatusCodeIs500() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(500).setBody("error")); getFetcher().loadData(Priority.NORMAL, callback); waitForResponseLatch.await(); verify(callback).onLoadFailed(isA(VolleyError.class)); } @Test public void testCallsLoadFailedIfStatusCodeIs400() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody("error")); getFetcher().loadData(Priority.LOW, callback); waitForResponseLatch.await(); verify(callback).onLoadFailed(isA(VolleyError.class)); } @Test public void testAppliesHeadersInGlideUrl() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(200)); String headerField = "field"; String headerValue = "value"; Map headersMap = new HashMap<>(); headersMap.put(headerField, headerValue); Headers headers = mock(Headers.class); when(headers.getHeaders()).thenReturn(headersMap); getFetcher(headers).loadData(Priority.HIGH, callback); waitForResponseLatch.await(); assertThat(mockWebServer.takeRequest().getHeader(headerField)).isEqualTo(headerValue); } private DataFetcher getFetcher() { return getFetcher(Headers.DEFAULT); } private DataFetcher getFetcher(Headers headers) { URL url = mockWebServer.url(DEFAULT_PATH).url(); return new VolleyStreamFetcher(requestQueue, new GlideUrl(url.toString(), headers)); } private class CountDown implements Answer { @Override public Void answer(InvocationOnMock invocation) throws Throwable { waitForResponseLatch.countDown(); return null; } } /** A shadow clock that doesn't rely on running on an Android thread with a Looper. */ @Implements(SystemClock.class) public static class FakeSystemClock extends ShadowSystemClock { // Used by Shadow. @SuppressWarnings("unused") @Implementation public static long elapsedRealtime() { // The default is to return something using the main looper, which doesn't exist on // Volley's threads. return System.currentTimeMillis(); } } } ================================================ FILE: library/build.gradle ================================================ apply plugin: 'com.android.library' if (!hasProperty('DISABLE_ERROR_PRONE')) { apply plugin: "net.ltgt.errorprone" } tasks.withType(JavaCompile) { options.fork = true } dependencies { api project(':third_party:gif_decoder') api project(':third_party:disklrucache') api project(':annotation') api libs.androidx.fragment api libs.androidx.vectordrawable api libs.androidx.exifinterface api libs.androidx.tracing compileOnly libs.androidx.appcompat if (project.plugins.hasPlugin('net.ltgt.errorprone')) { errorprone libs.errorprone.core } testImplementation libs.androidx.appcompat testImplementation project(':testutil') testImplementation libs.guava.testlib testImplementation libs.truth testImplementation libs.junit testImplementation libs.mockito.core testImplementation libs.robolectric testImplementation libs.mockwebserver testImplementation libs.androidx.test.core testImplementation libs.androidx.junit testImplementation libs.androidx.test.runner } if (project.plugins.hasPlugin('net.ltgt.errorprone')) { tasks.withType(JavaCompile) { options.errorprone.disable( // It's often useful to track individual objects when debugging // object pooling. "ObjectToString", // Doesn't apply when we can't use lambadas. "UnnecessaryAnonymousClass", // TODO(judds): Fix these and re-enable this check "BadImport", "UnescapedEntity", "MissingSummary", "InlineMeSuggester", "CanIgnoreReturnValueSuggester", "TypeNameShadowing", "UndefinedEquals", "UnnecessaryParentheses", "UnusedVariable", "EqualsGetClass", "LockNotBeforeTry") } } android { namespace 'com.bumptech.glide' compileSdkVersion libs.versions.compile.sdk.version.get() defaultConfig { minSdk libs.versions.min.sdk.version.get() as int targetSdk libs.versions.target.sdk.version.get() as int versionName VERSION_NAME as String consumerProguardFiles 'proguard-rules.txt' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } // Change the name to make it a little more obvious where the main library // documentation has gone. Using a capital letter happens to make this first in // the list too... afterEvaluate { dokkaHtmlPartial.configure { dokkaSourceSets { named("main") { moduleName.set("Glide") } } } } check.dependsOn(':library:pmd:pmd') check.dependsOn(':library:test:check') // Used in pmd and findbugs subprojects. @SuppressWarnings("GroovyUnusedDeclaration") def classPathForQuality() { return files( android.bootClasspath, project.android.libraryVariants.collect { it.javaCompile.classpath } ) } apply from: "${rootProject.projectDir}/scripts/upload.gradle.kts" ================================================ FILE: library/gradle.properties ================================================ POM_NAME=Glide POM_ARTIFACT_ID=glide POM_PACKAGING=aar # Prefix and postfix for source and javadoc jars. JAR_PREFIX=glide- JAR_POSTFIX= ================================================ FILE: library/lint.xml ================================================ ================================================ FILE: library/pmd/build.gradle ================================================ apply plugin: 'pmd' def library = project(':library') pmd { toolVersion libs.versions.pmd.get() } tasks.create('pmd', Pmd) { dependsOn library.tasks.compileReleaseJavaWithJavac targetJdk = TargetJdk.VERSION_1_7 description 'Run pmd' group 'verification' // If ruleSets is not empty, it seems to contain some // defaults which override rules in the ruleset file... ruleSets = [] ruleSetFiles = files("${library.projectDir}/pmd-ruleset.xml") source library.android.sourceSets.main.java.srcDirs classpath = files() classpath += files(library.tasks.compileReleaseJavaWithJavac.destinationDir) doFirst { classpath += library.classPathForQuality() } //TODO enable this once new Gradle containing this flag is out //see https://github.com/gradle/gradle/pull/3125#issuecomment-352442432 //incrementalAnalysis = true // Failures are caught and printed by the violations plugin. ignoreFailures = true reports { xml.required.set(true) html.required.set(false) } } ================================================ FILE: library/pmd-ruleset.xml ================================================ Check for flaws in Glide's codebase. ================================================ FILE: library/proguard-rules.txt ================================================ -keep public class * implements com.bumptech.glide.module.GlideModule -keep class * extends com.bumptech.glide.module.AppGlideModule { (...); } -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { **[] $VALUES; public *; } -keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { *** rewind(); } # Uncomment for DexGuard only #-keepresourcexmlelements manifest/application/meta-data@value=GlideModule ================================================ FILE: library/src/main/java/com/bumptech/glide/GeneratedAppGlideModule.java ================================================ package com.bumptech.glide; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.manager.RequestManagerRetriever; import com.bumptech.glide.module.AppGlideModule; import java.util.HashSet; import java.util.Set; /** * Allows {@link AppGlideModule}s to exclude {@link com.bumptech.glide.annotation.GlideModule}s to * ease the migration from {@link com.bumptech.glide.annotation.GlideModule}s to Glide's annotation * processing system and optionally provides a {@link * com.bumptech.glide.manager.RequestManagerRetriever.RequestManagerFactory} impl. */ abstract class GeneratedAppGlideModule extends AppGlideModule { /** This method can be removed when manifest parsing is no longer supported. */ @NonNull Set> getExcludedModuleClasses() { return new HashSet<>(); } @Nullable RequestManagerRetriever.RequestManagerFactory getRequestManagerFactory() { return null; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/GenericTransitionOptions.java ================================================ package com.bumptech.glide; import androidx.annotation.NonNull; import com.bumptech.glide.request.transition.TransitionFactory; import com.bumptech.glide.request.transition.ViewPropertyTransition; /** * Implementation of {@link TransitionOptions} that exposes only generic methods that can be applied * to any resource type. * * @param The type of the resource that will be displayed. */ // Public API. @SuppressWarnings({"PMD.UseUtilityClass", "unused"}) public final class GenericTransitionOptions extends TransitionOptions, TranscodeType> { /** * Removes any existing animation put on the builder. * * @see GenericTransitionOptions#dontTransition() */ @NonNull public static GenericTransitionOptions withNoTransition() { return new GenericTransitionOptions().dontTransition(); } /** * Returns a typed {@link GenericTransitionOptions} object that uses the given view animation. * * @see GenericTransitionOptions#transition(int) */ @NonNull public static GenericTransitionOptions with(int viewAnimationId) { return new GenericTransitionOptions().transition(viewAnimationId); } /** * Returns a typed {@link GenericTransitionOptions} object that uses the given animator. * * @see GenericTransitionOptions#transition(ViewPropertyTransition.Animator) */ @NonNull public static GenericTransitionOptions with( @NonNull ViewPropertyTransition.Animator animator) { return new GenericTransitionOptions().transition(animator); } /** * Returns a typed {@link GenericTransitionOptions} object that uses the given transition factory. * * @see GenericTransitionOptions#transition(TransitionFactory) */ @NonNull public static GenericTransitionOptions with( @NonNull TransitionFactory transitionFactory) { return new GenericTransitionOptions().transition(transitionFactory); } // Make sure that we're not equal to any other concrete implementation of TransitionOptions. @Override public boolean equals(Object o) { return o instanceof GenericTransitionOptions && super.equals(o); } // Our class doesn't include any additional properties, so we don't need to modify hashcode, but // keep it here as a reminder in case we add properties. @SuppressWarnings("PMD.UselessOverridingMethod") @Override public int hashCode() { return super.hashCode(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/Glide.java ================================================ package com.bumptech.glide; import android.app.Activity; import android.app.Application; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.os.Bundle; import android.os.MessageQueue.IdleHandler; import android.util.Log; import android.view.View; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.load.engine.prefill.BitmapPreFiller; import com.bumptech.glide.load.engine.prefill.PreFillType; import com.bumptech.glide.load.engine.prefill.PreFillType.Builder; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.load.resource.bitmap.HardwareConfigState; import com.bumptech.glide.manager.ConnectivityMonitorFactory; import com.bumptech.glide.manager.RequestManagerRetriever; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.GlideModule; import com.bumptech.glide.module.ManifestParser; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.ImageViewTargetFactory; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.GlideSuppliers; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * A singleton to present a simple static interface for building requests with {@link * RequestBuilder} and maintaining an {@link Engine}, {@link BitmapPool}, {@link * com.bumptech.glide.load.engine.cache.DiskCache} and {@link MemoryCache}. */ public class Glide implements ComponentCallbacks2 { private static final String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache"; private static final String DESTROYED_ACTIVITY_WARNING = "You cannot start a load on a not yet attached View or a Fragment where getActivity() " + "returns null (which usually occurs when getActivity() is called before the Fragment " + "is attached or after the Fragment is destroyed)."; private static final String TAG = "Glide"; @GuardedBy("Glide.class") private static volatile Glide glide; private static volatile boolean isInitializing; private final Engine engine; private final BitmapPool bitmapPool; private final MemoryCache memoryCache; private final GlideContext glideContext; private final ArrayPool arrayPool; private final RequestManagerRetriever requestManagerRetriever; private final ConnectivityMonitorFactory connectivityMonitorFactory; @GuardedBy("managers") private final List managers = new ArrayList<>(); private final RequestOptionsFactory defaultRequestOptionsFactory; private MemoryCategory memoryCategory = MemoryCategory.NORMAL; @GuardedBy("this") @Nullable private BitmapPreFiller bitmapPreFiller; private boolean inBackground = false; private MemoryCategory memoryCategoryInBackground = null; private MemoryCategory memoryCategoryInForeground = MemoryCategory.NORMAL; private final GlideSupplier setMemoryCategoryCallbacks = GlideSuppliers.memorize(SetMemoryCategoryOnLifecycleCallbacks::new); /** * Returns a directory with a default name in the private cache directory of the application to * use to store retrieved media and thumbnails. * * @param context A context. * @see #getPhotoCacheDir(android.content.Context, String) */ @Nullable public static File getPhotoCacheDir(@NonNull Context context) { return getPhotoCacheDir(context, DEFAULT_DISK_CACHE_DIR); } /** * Returns a directory with the given name in the private cache directory of the application to * use to store retrieved media and thumbnails. * * @param context A context. * @param cacheName The name of the subdirectory in which to store the cache. * @see #getPhotoCacheDir(android.content.Context) */ @Nullable public static File getPhotoCacheDir(@NonNull Context context, @NonNull String cacheName) { File cacheDir = context.getCacheDir(); if (cacheDir != null) { File result = new File(cacheDir, cacheName); if (result.isDirectory() || result.mkdirs()) { return result; } // File wasn't able to create a directory, or the result exists but not a directory return null; } if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "default disk cache dir is null"); } return null; } /** * Get the singleton. * * @return the singleton */ @NonNull // Double checked locking is safe here. @SuppressWarnings("GuardedBy") public static Glide get(@NonNull Context context) { if (glide == null) { GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules(context.getApplicationContext()); synchronized (Glide.class) { if (glide == null) { checkAndInitializeGlide(context, annotationGeneratedModule); } } } return glide; } @GuardedBy("Glide.class") @VisibleForTesting static void checkAndInitializeGlide( @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) { // In the thread running initGlide(), one or more classes may call Glide.get(context). // Without this check, those calls could trigger infinite recursion. if (isInitializing) { throw new IllegalStateException( "Glide has been called recursively, this is probably an internal library error!"); } isInitializing = true; try { initializeGlide(context, generatedAppGlideModule); } finally { isInitializing = false; } } /** * @deprecated Use {@link #init(Context, GlideBuilder)} to get a singleton compatible with Glide's * generated API. *

    This method will be removed in a future version of Glide. */ @VisibleForTesting @Deprecated public static synchronized void init(Glide glide) { if (Glide.glide != null) { tearDown(); } Glide.glide = glide; } @VisibleForTesting public static void init(@NonNull Context context, @NonNull GlideBuilder builder) { GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules(context); synchronized (Glide.class) { if (Glide.glide != null) { tearDown(); } initializeGlide(context, builder, annotationGeneratedModule); } } @VisibleForTesting public static synchronized boolean isInitialized() { return glide != null; } /** * Allows hardware Bitmaps to be used prior to the first frame in the app being drawn as soon as * this method is called. * *

    If you use this method in non-test code, your app will experience native crashes on some * versions of Android if you try to decode a hardware Bitmap. This method is only useful for * testing. */ @VisibleForTesting public static void enableHardwareBitmaps() { HardwareConfigState.getInstance().unblockHardwareBitmaps(); } @VisibleForTesting public static void tearDown() { synchronized (Glide.class) { if (glide != null) { glide.getContext().getApplicationContext().unregisterComponentCallbacks(glide); glide.unregisterActivityLifecycleCallbacks(); glide.engine.shutdown(); } glide = null; } } @GuardedBy("Glide.class") private static void initializeGlide( @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) { initializeGlide(context, new GlideBuilder(), generatedAppGlideModule); } @GuardedBy("Glide.class") @SuppressWarnings("deprecation") private static void initializeGlide( @NonNull Context context, @NonNull GlideBuilder builder, @Nullable GeneratedAppGlideModule annotationGeneratedModule) { Context applicationContext = context.getApplicationContext(); List manifestModules = Collections.emptyList(); if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) { manifestModules = new ManifestParser(applicationContext).parse(); } if (annotationGeneratedModule != null && !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) { Set> excludedModuleClasses = annotationGeneratedModule.getExcludedModuleClasses(); Iterator iterator = manifestModules.iterator(); while (iterator.hasNext()) { GlideModule current = iterator.next(); if (!excludedModuleClasses.contains(current.getClass())) { continue; } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "AppGlideModule excludes manifest GlideModule: " + current); } iterator.remove(); } } if (Log.isLoggable(TAG, Log.DEBUG)) { for (GlideModule glideModule : manifestModules) { Log.d(TAG, "Discovered GlideModule from manifest: " + glideModule.getClass()); } } RequestManagerRetriever.RequestManagerFactory factory = annotationGeneratedModule != null ? annotationGeneratedModule.getRequestManagerFactory() : null; builder.setRequestManagerFactory(factory); for (GlideModule module : manifestModules) { module.applyOptions(applicationContext, builder); } if (annotationGeneratedModule != null) { annotationGeneratedModule.applyOptions(applicationContext, builder); } Glide glide = builder.build(applicationContext, manifestModules, annotationGeneratedModule); applicationContext.registerComponentCallbacks(glide); glide.registerActivityLifecycleCallbacks(); Glide.glide = glide; } @Nullable @SuppressWarnings({"unchecked", "TryWithIdenticalCatches", "PMD.UnusedFormalParameter"}) private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules(Context context) { GeneratedAppGlideModule result = null; try { Class clazz = (Class) Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl"); result = clazz.getDeclaredConstructor(Context.class).newInstance(context.getApplicationContext()); } catch (ClassNotFoundException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w( TAG, "Failed to find GeneratedAppGlideModule. You should include an" + " annotationProcessor compile dependency on com.github.bumptech.glide:compiler" + " in your application and a @GlideModule annotated AppGlideModule implementation" + " or LibraryGlideModules will be silently ignored"); } // These exceptions can't be squashed across all versions of Android. } catch (InstantiationException e) { throwIncorrectGlideModule(e); } catch (IllegalAccessException e) { throwIncorrectGlideModule(e); } catch (NoSuchMethodException e) { throwIncorrectGlideModule(e); } catch (InvocationTargetException e) { throwIncorrectGlideModule(e); } return result; } private static void throwIncorrectGlideModule(Exception e) { throw new IllegalStateException( "GeneratedAppGlideModuleImpl is implemented incorrectly." + " If you've manually implemented this class, remove your implementation. The" + " Annotation processor will generate a correct implementation.", e); } @SuppressWarnings("PMD.UnusedFormalParameter") Glide( @NonNull Context context, @NonNull Engine engine, @NonNull MemoryCache memoryCache, @NonNull BitmapPool bitmapPool, @NonNull ArrayPool arrayPool, @NonNull RequestManagerRetriever requestManagerRetriever, @NonNull ConnectivityMonitorFactory connectivityMonitorFactory, int logLevel, @NonNull RequestOptionsFactory defaultRequestOptionsFactory, @NonNull Map, TransitionOptions> defaultTransitionOptions, @NonNull List> defaultRequestListeners, @NonNull List manifestModules, @Nullable AppGlideModule annotationGeneratedModule, @NonNull GlideExperiments experiments) { this.engine = engine; this.bitmapPool = bitmapPool; this.arrayPool = arrayPool; this.memoryCache = memoryCache; this.requestManagerRetriever = requestManagerRetriever; this.connectivityMonitorFactory = connectivityMonitorFactory; this.defaultRequestOptionsFactory = defaultRequestOptionsFactory; GlideBuilder.MemoryCategoryInBackground memoryCategoryInBackground = experiments.get(GlideBuilder.MemoryCategoryInBackground.class); if (memoryCategoryInBackground != null) { this.memoryCategoryInBackground = memoryCategoryInBackground.value(); } // This has a circular relationship with Glide and GlideContext in that it depends on both, // but it's created by Glide's constructor. In practice this shouldn't matter because the // supplier holding the registry should never be initialized before this constructor finishes. GlideSupplier registry = RegistryFactory.lazilyCreateAndInitializeRegistry( this, manifestModules, annotationGeneratedModule); ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory(); glideContext = new GlideContext( context, arrayPool, registry, imageViewTargetFactory, defaultRequestOptionsFactory, defaultTransitionOptions, defaultRequestListeners, engine, experiments, logLevel); } /** * Returns the {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} used to * temporarily store {@link android.graphics.Bitmap}s so they can be reused to avoid garbage * collections. * *

    Note - Using this pool directly can lead to undefined behavior and strange drawing errors. * Any {@link android.graphics.Bitmap} added to the pool must not be currently in use in any other * part of the application. Any {@link android.graphics.Bitmap} added to the pool must be removed * from the pool before it is added a second time. * *

    Note - To make effective use of the pool, any {@link android.graphics.Bitmap} removed from * the pool must eventually be re-added. Otherwise the pool will eventually empty and will not * serve any useful purpose. * *

    The primary reason this object is exposed is for use in custom {@link * com.bumptech.glide.load.ResourceDecoder}s and {@link com.bumptech.glide.load.Transformation}s. * Use outside of these classes is not generally recommended. */ @NonNull public BitmapPool getBitmapPool() { return bitmapPool; } @NonNull public ArrayPool getArrayPool() { return arrayPool; } /** * @return The context associated with this instance. */ @NonNull public Context getContext() { return glideContext.getBaseContext(); } ConnectivityMonitorFactory getConnectivityMonitorFactory() { return connectivityMonitorFactory; } @NonNull GlideContext getGlideContext() { return glideContext; } /** * Pre-fills the {@link BitmapPool} using the given sizes. * *

    Enough Bitmaps are added to completely fill the pool, so most or all of the Bitmaps * currently in the pool will be evicted. Bitmaps are allocated according to the weights of the * given sizes, where each size gets (weight / prefillWeightSum) percent of the pool to fill. * *

    Note - Pre-filling is done asynchronously using and {@link IdleHandler}. Any currently * running pre-fill will be cancelled and replaced by a call to this method. * *

    This method should be used with caution, overly aggressive pre-filling is substantially * worse than not pre-filling at all. Pre-filling should only be started in onCreate to avoid * constantly clearing and re-filling the {@link BitmapPool}. Rotation should be carefully * considered as well. It may be worth calling this method only when no saved instance state * exists so that pre-filling only happens when the Activity is first created, rather than on * every rotation. * * @param bitmapAttributeBuilders The list of {@link Builder Builders} representing individual * sizes and configurations of {@link Bitmap}s to be pre-filled. */ @SuppressWarnings("unused") // Public API public synchronized void preFillBitmapPool( @NonNull PreFillType.Builder... bitmapAttributeBuilders) { if (bitmapPreFiller == null) { DecodeFormat decodeFormat = defaultRequestOptionsFactory.build().getOptions().get(Downsampler.DECODE_FORMAT); bitmapPreFiller = new BitmapPreFiller(memoryCache, bitmapPool, decodeFormat); } bitmapPreFiller.preFill(bitmapAttributeBuilders); } /** * Clears as much memory as possible. * * @see android.content.ComponentCallbacks#onLowMemory() * @see android.content.ComponentCallbacks2#onLowMemory() */ public void clearMemory() { // Engine asserts this anyway when removing resources, fail faster and consistently Util.assertMainThread(); // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687. memoryCache.clearMemory(); bitmapPool.clearMemory(); arrayPool.clearMemory(); } /** * Clears some memory with the exact amount depending on the given level. * * @see android.content.ComponentCallbacks2#onTrimMemory(int) */ public void trimMemory(int level) { // Engine asserts this anyway when removing resources, fail faster and consistently Util.assertMainThread(); // Request managers need to be trimmed before the caches and pools, in order for the latter to // have the most benefit. synchronized (managers) { for (RequestManager manager : managers) { manager.onTrimMemory(level); } } // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687. memoryCache.trimMemory(level); bitmapPool.trimMemory(level); arrayPool.trimMemory(level); } /** * Clears disk cache. * *

    This method should always be called on a background thread, since it is a blocking call. */ // Public API. @SuppressWarnings({"unused", "WeakerAccess"}) public void clearDiskCache() { Util.assertBackgroundThread(); engine.clearDiskCache(); } /** Internal method. */ @NonNull public RequestManagerRetriever getRequestManagerRetriever() { return requestManagerRetriever; } /** * Adjusts Glide's current and maximum memory usage based on the given {@link MemoryCategory}. * *

    The default {@link MemoryCategory} is {@link MemoryCategory#NORMAL}. {@link * MemoryCategory#HIGH} increases Glide's maximum memory usage by up to 50% and {@link * MemoryCategory#LOW} decreases Glide's maximum memory usage by 50%. This method should be used * to temporarily increase or decrease memory usage for a single Activity or part of the app. Use * {@link GlideBuilder#setMemoryCache(MemoryCache)} to put a permanent memory size if you want to * change the default. * * @return the previous MemoryCategory used by Glide. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull public MemoryCategory setMemoryCategory(@NonNull MemoryCategory memoryCategory) { // Engine asserts this anyway when removing resources, fail faster and consistently Util.assertMainThread(); // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687. memoryCache.setSizeMultiplier(memoryCategory.getMultiplier()); bitmapPool.setSizeMultiplier(memoryCategory.getMultiplier()); MemoryCategory oldCategory = this.memoryCategory; this.memoryCategory = memoryCategory; return oldCategory; } @NonNull private static RequestManagerRetriever getRetriever(@Nullable Context context) { // Context could be null for other reasons (ie the user passes in null), but in practice it will // only occur due to errors with the Fragment lifecycle. Preconditions.checkNotNull(context, DESTROYED_ACTIVITY_WARNING); return Glide.get(context).getRequestManagerRetriever(); } /** * Begin a load with Glide by passing in a context. * *

    Any requests started using a context will only have the application level options applied * and will not be started or stopped based on lifecycle events. In general, loads should be * started at the level the result will be used in. If the resource will be used in a view in a * child fragment, the load should be started with {@link #with(android.app.Fragment)}} using that * child fragment. Similarly, if the resource will be used in a view in the parent fragment, the * load should be started with {@link #with(android.app.Fragment)} using the parent fragment. In * the same vein, if the resource will be used in a view in an activity, the load should be * started with {@link #with(android.app.Activity)}}. * *

    This method is appropriate for resources that will be used outside of the normal fragment or * activity lifecycle (For example in services, or for notification thumbnails). * * @param context Any context, will not be retained. * @return A RequestManager for the top level application that can be used to start a load. * @see #with(android.app.Activity) * @see #with(android.app.Fragment) * @see #with(androidx.fragment.app.Fragment) * @see #with(androidx.fragment.app.FragmentActivity) */ @NonNull public static RequestManager with(@NonNull Context context) { return getRetriever(context).get(context); } /** * Begin a load with Glide that will be tied to the given {@link android.app.Activity}'s lifecycle * and that uses the given {@link Activity}'s default options. * * @param activity The activity to use. * @return A RequestManager for the given activity that can be used to start a load. * @deprecated This is equivalent to calling {@link #with(Context)} using the application context. * Use the androidx Activity class instead (ie {@link FragmentActivity}, or {@link * androidx.appcompat.app.AppCompatActivity}). * @throws IllegalArgumentException if the activity associated with the Glide request is being * destroyed. */ @NonNull @Deprecated public static RequestManager with(@NonNull Activity activity) { return with(activity.getApplicationContext()); } /** * Begin a load with Glide that will tied to the give {@link * androidx.fragment.app.FragmentActivity}'s lifecycle and that uses the given {@link * androidx.fragment.app.FragmentActivity}'s default options. * * @param activity The activity to use. The activity must not be destroyed. * @return A RequestManager for the given FragmentActivity that can be used to start a load. * @throws IllegalArgumentException if the activity is being destroyed. */ @NonNull public static RequestManager with(@NonNull FragmentActivity activity) { return getRetriever(activity).get(activity); } /** * Begin a load with Glide that will be tied to the given {@link androidx.fragment.app.Fragment}'s * lifecycle and that uses the given {@link androidx.fragment.app.Fragment}'s default options. * * @param fragment The fragment to use. * @return A RequestManager for the given Fragment that can be used to start a load. * @throws IllegalArgumentException if the activity associated with the fragment is destroyed. */ @NonNull public static RequestManager with(@NonNull Fragment fragment) { return getRetriever(fragment.getContext()).get(fragment); } /** * Begin a load with Glide that will be tied to the given {@link android.app.Fragment}'s lifecycle * and that uses the given {@link android.app.Fragment}'s default options. * * @param fragment The fragment to use. * @return A RequestManager for the given Fragment that can be used to start a load. * @deprecated This method is identical to calling {@link Glide#with(Context)} using the * application context. Prefer support Fragments and {@link #with(Fragment)} instead. See * https://github.com/android/android-ktx/pull/161#issuecomment-363270555. * @throws IllegalArgumentException if the activity associated with the fragment is destroyed. */ @Deprecated @NonNull public static RequestManager with(@NonNull android.app.Fragment fragment) { Activity activity = fragment.getActivity(); Preconditions.checkNotNull(activity, DESTROYED_ACTIVITY_WARNING); return with(activity.getApplicationContext()); } /** * Begin a load with Glide that will be tied to the lifecycle of the {@link Fragment}, {@link * android.app.Fragment}, or {@link Activity} that contains the View. * *

    A {@link Fragment} or {@link android.app.Fragment} is assumed to contain a View if the View * is a child of the View returned by the {@link Fragment#getView()}} method. * *

    This method will not work if the View is not attached. Prefer the Activity and Fragment * variants unless you're loading in a View subclass. * *

    This method may be inefficient aways and is definitely inefficient for large hierarchies. * Consider memoizing the result after the View is attached or again, prefer the Activity and * Fragment variants whenever possible. * *

    When used in Applications that use the non-support {@link android.app.Fragment} classes, * calling this method will produce noisy logs from {@link android.app.FragmentManager}. Consider * avoiding entirely or using the {@link Fragment}s from the support library instead. * *

    If the support {@link FragmentActivity} class is used, this method will only attempt to * discover support {@link Fragment}s. Any non-support {@link android.app.Fragment}s attached to * the {@link FragmentActivity} will be ignored. * * @param view The view to search for a containing Fragment or Activity from. * @return A RequestManager that can be used to start a load. * @throws IllegalArgumentException if the activity associated with the view is destroyed. */ @NonNull public static RequestManager with(@NonNull View view) { return getRetriever(view.getContext()).get(view); } @NonNull public Registry getRegistry() { return glideContext.getRegistry(); } boolean removeFromManagers(@NonNull Target target) { synchronized (managers) { for (RequestManager requestManager : managers) { if (requestManager.untrack(target)) { return true; } } } return false; } void registerRequestManager(RequestManager requestManager) { synchronized (managers) { if (managers.contains(requestManager)) { throw new IllegalStateException("Cannot register already registered manager"); } managers.add(requestManager); } } void unregisterRequestManager(RequestManager requestManager) { synchronized (managers) { if (!managers.contains(requestManager)) { throw new IllegalStateException("Cannot unregister not yet registered manager"); } managers.remove(requestManager); } } @Override public void onTrimMemory(int level) { trimMemory(level); // when level is TRIM_MEMORY_UI_HIDDEN or higher, it indicates that the app is // in the background, limit the memory usage by memoryCategoryInBackground. if (level >= TRIM_MEMORY_UI_HIDDEN) { setMemoryCategoryWhenInBackground(); } } @Override public void onConfigurationChanged(Configuration newConfig) { // Do nothing. } @Override public void onLowMemory() { clearMemory(); } /** Creates a new instance of {@link RequestOptions}. */ public interface RequestOptionsFactory { /** Returns a non-null {@link RequestOptions} object. */ @NonNull RequestOptions build(); } private void registerActivityLifecycleCallbacks() { if (memoryCategoryInBackground != null) { Context context = getContext().getApplicationContext(); if (!(context instanceof Application) && Log.isLoggable(TAG, Log.WARN)) { Log.w( TAG, "Glide requires an Application Context. You passed: " + context + ". This will disable setting memory category in background."); return; } ((Application) context).registerActivityLifecycleCallbacks(setMemoryCategoryCallbacks.get()); } } private void unregisterActivityLifecycleCallbacks() { if (memoryCategoryInBackground != null) { Context context = getContext().getApplicationContext(); if (context instanceof Application) { ((Application) context) .unregisterActivityLifecycleCallbacks(setMemoryCategoryCallbacks.get()); } } } private void setMemoryCategoryWhenInBackground() { if (memoryCategoryInBackground == null || inBackground) { return; } inBackground = true; memoryCategoryInForeground = setMemoryCategory(memoryCategoryInBackground); } private void setMemoryCategoryWhenInForeground() { if (memoryCategoryInBackground == null || !inBackground) { return; } inBackground = false; setMemoryCategory(memoryCategoryInForeground); } private final class SetMemoryCategoryOnLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { @Override public void onActivityStarted(Activity activity) { // Do nothing. } @Override public void onActivityResumed(Activity activity) { // Any activity resumed indicates that the app is no longer in the background, // and we should restore the memory usage to normal. setMemoryCategoryWhenInForeground(); } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { // Do nothing. } @Override public void onActivityDestroyed(Activity activity) { // Do nothing. } @Override public void onActivityStopped(Activity activity) { // Do nothing. } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { // Do nothing. } @Override public void onActivityPaused(Activity activity) { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/GlideBuilder.java ================================================ package com.bumptech.glide; import android.content.Context; import android.graphics.Bitmap; import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import com.bumptech.glide.Glide.RequestOptionsFactory; import com.bumptech.glide.GlideExperiments.Experiment; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; import com.bumptech.glide.load.engine.cache.LruResourceCache; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.load.engine.cache.MemorySizeCalculator; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.manager.ConnectivityMonitorFactory; import com.bumptech.glide.manager.DefaultConnectivityMonitorFactory; import com.bumptech.glide.manager.RequestManagerRetriever; import com.bumptech.glide.manager.RequestManagerRetriever.RequestManagerFactory; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.GlideModule; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.Preconditions; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** A builder class for setting default structural classes for Glide to use. */ @SuppressWarnings("PMD.ImmutableField") public final class GlideBuilder { private final Map, TransitionOptions> defaultTransitionOptions = new ArrayMap<>(); private final GlideExperiments.Builder glideExperimentsBuilder = new GlideExperiments.Builder(); private Engine engine; private BitmapPool bitmapPool; private ArrayPool arrayPool; private MemoryCache memoryCache; private GlideExecutor sourceExecutor; private GlideExecutor diskCacheExecutor; private DiskCache.Factory diskCacheFactory; private MemorySizeCalculator memorySizeCalculator; private ConnectivityMonitorFactory connectivityMonitorFactory; private int logLevel = Log.INFO; private RequestOptionsFactory defaultRequestOptionsFactory = new RequestOptionsFactory() { @NonNull @Override public RequestOptions build() { return new RequestOptions(); } }; @Nullable private RequestManagerFactory requestManagerFactory; private GlideExecutor animationExecutor; private boolean isActiveResourceRetentionAllowed; @Nullable private List> defaultRequestListeners; /** * Sets the {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} implementation to use * to store and retrieve reused {@link android.graphics.Bitmap}s. * * @param bitmapPool The pool to use. * @return This builder. */ @NonNull public GlideBuilder setBitmapPool(@Nullable BitmapPool bitmapPool) { this.bitmapPool = bitmapPool; return this; } /** * Sets the {@link ArrayPool} implementation to allow variable sized arrays to be stored and * retrieved as needed. * * @param arrayPool The pool to use. * @return This builder. */ @NonNull public GlideBuilder setArrayPool(@Nullable ArrayPool arrayPool) { this.arrayPool = arrayPool; return this; } /** * Sets the {@link com.bumptech.glide.load.engine.cache.MemoryCache} implementation to store * {@link com.bumptech.glide.load.engine.Resource}s that are not currently in use. * * @param memoryCache The cache to use. * @return This builder. */ // Public API. @SuppressWarnings("WeakerAccess") @NonNull public GlideBuilder setMemoryCache(@Nullable MemoryCache memoryCache) { this.memoryCache = memoryCache; return this; } /** * Sets the {@link com.bumptech.glide.load.engine.cache.DiskCache.Factory} implementation to use * to construct the {@link com.bumptech.glide.load.engine.cache.DiskCache} to use to store {@link * com.bumptech.glide.load.engine.Resource} data on disk. * * @param diskCacheFactory The disk cache factory to use. * @return This builder. */ // Public API. @SuppressWarnings("WeakerAccess") @NonNull public GlideBuilder setDiskCache(@Nullable DiskCache.Factory diskCacheFactory) { this.diskCacheFactory = diskCacheFactory; return this; } /** * Sets the {@link GlideExecutor} to use when retrieving {@link * com.bumptech.glide.load.engine.Resource}s that are not already in the cache. * *

    The thread count defaults to the number of cores available on the device, with a maximum of * 4. * *

    Use the {@link GlideExecutor#newSourceExecutor()} methods if you'd like to specify options * for the source executor. * * @param service The ExecutorService to use. * @return This builder. * @see #setDiskCacheExecutor(GlideExecutor) * @see GlideExecutor * @deprecated Use {@link #setSourceExecutor(GlideExecutor)} */ @Deprecated public GlideBuilder setResizeExecutor(@Nullable GlideExecutor service) { return setSourceExecutor(service); } /** * Sets the {@link GlideExecutor} to use when retrieving {@link * com.bumptech.glide.load.engine.Resource}s that are not already in the cache. * *

    The thread count defaults to the number of cores available on the device, with a maximum of * 4. * *

    Use the {@link GlideExecutor#newSourceExecutor()} methods if you'd like to specify options * for the source executor. * * @param service The ExecutorService to use. * @return This builder. * @see #setDiskCacheExecutor(GlideExecutor) * @see GlideExecutor */ // Public API. @SuppressWarnings("WeakerAccess") @NonNull public GlideBuilder setSourceExecutor(@Nullable GlideExecutor service) { this.sourceExecutor = service; return this; } /** * Sets the {@link GlideExecutor} to use when retrieving {@link * com.bumptech.glide.load.engine.Resource}s that are currently in Glide's disk caches. * *

    Defaults to a single thread which is usually the best combination of memory usage, jank, and * performance, even on high end devices. * *

    Use the {@link GlideExecutor#newDiskCacheExecutor()} if you'd like to specify options for * the disk cache executor. * * @param service The {@link GlideExecutor} to use. * @return This builder. * @see #setSourceExecutor(GlideExecutor) * @see GlideExecutor */ // Public API. @SuppressWarnings("WeakerAccess") @NonNull public GlideBuilder setDiskCacheExecutor(@Nullable GlideExecutor service) { this.diskCacheExecutor = service; return this; } /** * Sets the {@link GlideExecutor} to use when loading frames of animated images and particularly * of {@link com.bumptech.glide.load.resource.gif.GifDrawable}s. * *

    Defaults to one or two threads, depending on the number of cores available. * *

    Use the {@link GlideExecutor#newAnimationExecutor()} methods if you'd like to specify * options for the animation executor. * * @param service The {@link GlideExecutor} to use. * @return This builder. */ // Public API. @SuppressWarnings("WeakerAccess") @NonNull public GlideBuilder setAnimationExecutor(@Nullable GlideExecutor service) { this.animationExecutor = service; return this; } /** * Sets the default {@link RequestOptions} to use for all loads across the app. * *

    Applying additional options with {@link RequestBuilder#apply(BaseRequestOptions)} will * override defaults set here. * * @see #setDefaultRequestOptions(RequestOptionsFactory) * @param requestOptions The options to use by default. * @return This builder. */ @NonNull public GlideBuilder setDefaultRequestOptions(@Nullable final RequestOptions requestOptions) { return setDefaultRequestOptions( new RequestOptionsFactory() { @NonNull @Override public RequestOptions build() { return requestOptions != null ? requestOptions : new RequestOptions(); } }); } /** * Sets a factory for the default {@link RequestOptions} to use for all loads across the app and * returns this {@code GlideBuilder}. * *

    This factory will NOT be called once per load. Instead it will be called a handful * of times and memoized. It's not safe to assume that this factory will be called again for every * new load. * *

    Applying additional options with {@link RequestBuilder#apply(BaseRequestOptions)} will * override defaults set here. * * @see #setDefaultRequestOptions(RequestOptionsFactory) */ @NonNull public GlideBuilder setDefaultRequestOptions(@NonNull RequestOptionsFactory factory) { this.defaultRequestOptionsFactory = Preconditions.checkNotNull(factory); return this; } /** * Sets the default {@link TransitionOptions} to use when starting a request that will load a * resource with the given {@link Class}. * *

    It's preferable but not required for the requested resource class to match the resource * class applied here as long as the resource class applied here is assignable from the requested * resource class. For example you can set a default transition for {@link * android.graphics.drawable.Drawable} and that default transition will be used if you * subsequently start requests for specific {@link android.graphics.drawable.Drawable} types like * {@link com.bumptech.glide.load.resource.gif.GifDrawable} or {@link * android.graphics.drawable.BitmapDrawable}. Specific types are always preferred so if you * register a default transition for both {@link android.graphics.drawable.Drawable} and {@link * android.graphics.drawable.BitmapDrawable} and then start a request for {@link * android.graphics.drawable.BitmapDrawable}s, the transition you registered for {@link * android.graphics.drawable.BitmapDrawable}s will be used. */ // Public API. @SuppressWarnings("unused") @NonNull public GlideBuilder setDefaultTransitionOptions( @NonNull Class clazz, @Nullable TransitionOptions options) { defaultTransitionOptions.put(clazz, options); return this; } /** * Sets the {@link MemorySizeCalculator} to use to calculate maximum sizes for default {@link * MemoryCache MemoryCaches} and/or default {@link BitmapPool BitmapPools}. * * @see #setMemorySizeCalculator(MemorySizeCalculator) * @param builder The builder to use (will not be modified). * @return This builder. */ // Public API. @SuppressWarnings("unused") @NonNull public GlideBuilder setMemorySizeCalculator(@NonNull MemorySizeCalculator.Builder builder) { return setMemorySizeCalculator(builder.build()); } /** * Sets the {@link MemorySizeCalculator} to use to calculate maximum sizes for default {@link * MemoryCache MemoryCaches} and/or default {@link BitmapPool BitmapPools}. * *

    The given {@link MemorySizeCalculator} will not affect custom pools or caches provided via * {@link #setBitmapPool(BitmapPool)} or {@link #setMemoryCache(MemoryCache)}. * * @param calculator The calculator to use. * @return This builder. */ // Public API. @SuppressWarnings("WeakerAccess") @NonNull public GlideBuilder setMemorySizeCalculator(@Nullable MemorySizeCalculator calculator) { this.memorySizeCalculator = calculator; return this; } /** * Sets the {@link com.bumptech.glide.manager.ConnectivityMonitorFactory} to use to notify {@link * com.bumptech.glide.RequestManager} of connectivity events. If not set {@link * com.bumptech.glide.manager.DefaultConnectivityMonitorFactory} would be used. * * @param factory The factory to use * @return This builder. */ // Public API. @SuppressWarnings("unused") @NonNull public GlideBuilder setConnectivityMonitorFactory(@Nullable ConnectivityMonitorFactory factory) { this.connectivityMonitorFactory = factory; return this; } /** * Sets a log level constant from those in {@link Log} to indicate the desired log verbosity. * *

    The level must be one of {@link Log#VERBOSE}, {@link Log#DEBUG}, {@link Log#INFO}, {@link * Log#WARN}, or {@link Log#ERROR}. * *

    {@link Log#VERBOSE} means one or more lines will be logged per request, including timing * logs and failures. {@link Log#DEBUG} means at most one line will be logged per successful * request, including timing logs, although many lines may be logged for failures including * multiple complete stack traces. {@link Log#INFO} means failed loads will be logged including * multiple complete stack traces, but successful loads will not be logged at all. {@link * Log#WARN} means only summaries of failed loads will be logged. {@link Log#ERROR} means only * exceptional cases will be logged. * *

    All logs will be logged using the 'Glide' tag. * *

    Many other debugging logs are available in individual classes. The log level supplied here * only controls a small set of informative and well formatted logs. Users wishing to debug * certain aspects of the library can look for individual TAG variables at the tops * of classes and use adb shell setprop log.tag.TAG to enable or disable any relevant * tags. * * @param logLevel The log level to use from {@link Log}. * @return This builder. */ // Public API. @SuppressWarnings("unused") @NonNull public GlideBuilder setLogLevel(int logLevel) { if (logLevel < Log.VERBOSE || logLevel > Log.ERROR) { throw new IllegalArgumentException( "Log level must be one of Log.VERBOSE, Log.DEBUG," + " Log.INFO, Log.WARN, or Log.ERROR"); } this.logLevel = logLevel; return this; } /** * If set to {@code true}, allows Glide to re-capture resources that are loaded into {@link * com.bumptech.glide.request.target.Target}s which are subsequently de-referenced and garbage * collected without being cleared. * *

    Defaults to {@code false}. * *

    Glide's resource re-use system is permissive, which means that's acceptable for callers to * load resources into {@link com.bumptech.glide.request.target.Target}s and then never clear the * {@link com.bumptech.glide.request.target.Target}. To do so, Glide uses {@link * java.lang.ref.WeakReference}s to track resources that belong to {@link * com.bumptech.glide.request.target.Target}s that haven't yet been cleared. Setting this method * to {@code true} allows Glide to also maintain a hard reference to the underlying resource so * that if the {@link com.bumptech.glide.request.target.Target} is garbage collected, Glide can * return the underlying resource to it's memory cache so that subsequent requests will not * unexpectedly re-load the resource from disk or source. As a side affect, it will take the * system slightly longer to garbage collect the underlying resource because the weak reference * has to be cleared and processed before the hard reference is removed. As a result, setting this * method to {@code true} may transiently increase the memory usage of an application. * *

    Leaving this method at the default {@code false} value will allow the platform to garbage * collect resources more quickly, but will lead to unexpected memory cache misses if callers load * resources into {@link com.bumptech.glide.request.target.Target}s but never clear them. * *

    If you set this method to {@code true} you must not call {@link Bitmap#recycle()} * or mutate any Bitmaps returned by Glide. If this method is set to {@code false}, recycling or * mutating Bitmaps is inefficient but safe as long as you do not clear the corresponding {@link * com.bumptech.glide.request.target.Target} used to load the {@link Bitmap}. However, if you set * this method to {@code true} and recycle or mutate any returned {@link Bitmap}s or other mutable * resources, Glide may recover those resources and attempt to use them later on, resulting in * crashes, graphical corruption or undefined behavior. * *

    Regardless of what value this method is set to, it's always good practice to clear {@link * com.bumptech.glide.request.target.Target}s when you're done with the corresponding resource. * Clearing {@link com.bumptech.glide.request.target.Target}s allows Glide to maximize resource * re-use, minimize memory overhead and minimize unexpected behavior resulting from edge cases. If * you use {@link RequestManager#clear(Target)}, calling {@link Bitmap#recycle()} or mutating * {@link Bitmap}s is not only unsafe, it's also totally unnecessary and should be avoided. In all * cases, prefer {@link RequestManager#clear(Target)} to {@link Bitmap#recycle()}. * * @return This builder. */ // Public API. @SuppressWarnings("unused") @NonNull public GlideBuilder setIsActiveResourceRetentionAllowed( boolean isActiveResourceRetentionAllowed) { this.isActiveResourceRetentionAllowed = isActiveResourceRetentionAllowed; return this; } /** * Adds a global {@link RequestListener} that will be added to every request started with Glide. * *

    Multiple {@link RequestListener}s can be added here, in {@link RequestManager} scopes or to * individual {@link RequestBuilder}s. {@link RequestListener}s are called in the order they're * added. Even if an earlier {@link RequestListener} returns {@code true} from {@link * RequestListener#onLoadFailed(GlideException, Object, Target, boolean)} or {@link * RequestListener#onResourceReady(Object, Object, Target, DataSource, boolean)}, it will not * prevent subsequent {@link RequestListener}s from being called. * *

    Because Glide requests can be started for any number of individual resource types, any * listener added here has to accept any generic resource type in {@link * RequestListener#onResourceReady(Object, Object, Target, DataSource, boolean)}. If you must base * the behavior of the listener on the resource type, you will need to use {@code instanceof} to * do so. It's not safe to cast resource types without first checking with {@code instanceof}. */ @NonNull public GlideBuilder addGlobalRequestListener(@NonNull RequestListener listener) { if (defaultRequestListeners == null) { defaultRequestListeners = new ArrayList<>(); } defaultRequestListeners.add(listener); return this; } /** * Set to {@code true} to make Glide populate {@link * com.bumptech.glide.load.engine.GlideException#setOrigin(Exception)} for failed requests. * *

    The exception set by this method is not printed by {@link GlideException} and can only be * viewed via a {@link RequestListener} that reads the field via {@link * GlideException#getOrigin()}. * *

    This is an experimental API that may be removed in the future. */ public GlideBuilder setLogRequestOrigins(boolean isEnabled) { glideExperimentsBuilder.update(new LogRequestOrigins(), isEnabled); return this; } /** * Set to {@code true} to make Glide use {@link android.graphics.ImageDecoder} when decoding * {@link Bitmap}s on Android P and higher. * *

    Calls to this method on versions of Android less than Q are ignored. Although ImageDecoder * was added in Android O a bug prevents it from scaling images with exif orientations until Q. * See b/136096254. * *

    Specifically {@link android.graphics.ImageDecoder} will be used in place of {@link * com.bumptech.glide.load.resource.bitmap.Downsampler} and {@link android.graphics.BitmapFactory} * to decode {@link Bitmap}s. GIFs, resources, and all other types of {@link * android.graphics.drawable.Drawable}s are not affected by this flag. * *

    This flag is experimental and may be removed without deprecation in a future version. * *

    When this flag is enabled, Bitmap's will not be re-used when decoding images, though they * may still be used as part of {@link com.bumptech.glide.load.Transformation}s because {@link * android.graphics.ImageDecoder} does not support Bitmap re-use. * *

    When this flag is enabled {@link * com.bumptech.glide.load.resource.bitmap.Downsampler#FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS} is * ignored. All other {@link com.bumptech.glide.load.resource.bitmap.Downsampler} flags are * obeyed, although there may be subtle behavior differences because many options are subject to * the whims of {@link android.graphics.BitmapFactory} and {@link android.graphics.ImageDecoder} * which may not agree. */ public GlideBuilder setImageDecoderEnabledForBitmaps(boolean isEnabled) { glideExperimentsBuilder.update( new EnableImageDecoderForBitmaps(), /* isEnabled= */ isEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); return this; } /** * Override the OS thread priority of threads created in {@link * com.bumptech.glide.load.engine.executor.GlideExecutor#DefaultThreadFactory} with {@link * com.bumptech.glide.load.engine.DecodeJob#GLIDE_THREAD_PRIORITY_OVERRIDE} Glide Option. * *

    This is an experimental API that may be removed in the future. */ public GlideBuilder setOverrideGlideThreadPriority(boolean isEnabled) { glideExperimentsBuilder.update(new OverrideGlideThreadPriority(), isEnabled); return this; } /** * Set to {@code true} to make Glide use {@link * android.provider.MediaStore#openAssetFileDescriptor(ContentResolver, Uri, String, * CancellationSignal)} when opening {@link android.provider.MediaStore#AUTHORITY} content URIs * when it is available. * *

    This is an experimental API that may be removed in the future. */ public GlideBuilder setUseMediaStoreOpenFileApisIfPossible(boolean isEnabled) { glideExperimentsBuilder.update(new UseMediaStoreOpenFileApisIfPossible(), isEnabled); return this; } /** * Set to {@code true} to make Glide use {@link MemoryCategory} to set the memory category when * the app is in the background. * *

    This is an experimental API that may be removed in the future. */ public GlideBuilder setMemoryCategoryInBackground(MemoryCategory memoryCategory) { glideExperimentsBuilder.add(new MemoryCategoryInBackground(memoryCategory)); return this; } /** * @deprecated This method does nothing. It will be hard coded and removed in a future release * without further warning. */ @Deprecated public GlideBuilder setPreserveGainmapAndColorSpaceForTransformations(boolean isEnabled) { return this; } /** * @deprecated This method does nothing. It will be hard coded and removed in a future release * without further warning. */ @Deprecated public GlideBuilder setEnableHardwareGainmapFixOnU(boolean isEnabled) { return this; } /** * @deprecated This method does nothing. It will be hard coded and removed in a future release * without further warning. */ @Deprecated public GlideBuilder setDisableHardwareBitmapsOnO(boolean disableHardwareBitmapsOnO) { return this; } void setRequestManagerFactory(@Nullable RequestManagerFactory factory) { this.requestManagerFactory = factory; } // For testing. GlideBuilder setEngine(Engine engine) { this.engine = engine; return this; } @NonNull Glide build( @NonNull Context context, List manifestModules, AppGlideModule annotationGeneratedGlideModule) { if (sourceExecutor == null) { sourceExecutor = GlideExecutor.newSourceExecutor(); } if (diskCacheExecutor == null) { diskCacheExecutor = GlideExecutor.newDiskCacheExecutor(); } if (animationExecutor == null) { animationExecutor = GlideExecutor.newAnimationExecutor(); } if (memorySizeCalculator == null) { memorySizeCalculator = new MemorySizeCalculator.Builder(context).build(); } if (connectivityMonitorFactory == null) { connectivityMonitorFactory = new DefaultConnectivityMonitorFactory(); } if (bitmapPool == null) { int size = memorySizeCalculator.getBitmapPoolSize(); if (size > 0) { bitmapPool = new LruBitmapPool(size); } else { bitmapPool = new BitmapPoolAdapter(); } } if (arrayPool == null) { arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes()); } if (memoryCache == null) { memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize()); } if (diskCacheFactory == null) { diskCacheFactory = new InternalCacheDiskCacheFactory(context); } if (engine == null) { engine = new Engine( memoryCache, diskCacheFactory, diskCacheExecutor, sourceExecutor, GlideExecutor.newUnlimitedSourceExecutor(), animationExecutor, isActiveResourceRetentionAllowed); } if (defaultRequestListeners == null) { defaultRequestListeners = Collections.emptyList(); } else { defaultRequestListeners = Collections.unmodifiableList(defaultRequestListeners); } GlideExperiments experiments = glideExperimentsBuilder.build(); RequestManagerRetriever requestManagerRetriever = new RequestManagerRetriever(requestManagerFactory); return new Glide( context, engine, memoryCache, bitmapPool, arrayPool, requestManagerRetriever, connectivityMonitorFactory, logLevel, defaultRequestOptionsFactory, defaultTransitionOptions, defaultRequestListeners, manifestModules, annotationGeneratedGlideModule, experiments); } static final class ManualOverrideHardwareBitmapMaxFdCount implements Experiment { final int fdCount; ManualOverrideHardwareBitmapMaxFdCount(int fdCount) { this.fdCount = fdCount; } } static final class EnableImageDecoderForBitmaps implements Experiment {} /** See {@link #setLogRequestOrigins(boolean)}. */ public static final class LogRequestOrigins implements Experiment {} /** See {@link #setOverrideGlideThreadPriority(boolean)}. */ public static final class OverrideGlideThreadPriority implements Experiment {} /** See {@link #setUseMediaStoreOpenFileApisIfPossible(boolean)}. */ public static final class UseMediaStoreOpenFileApisIfPossible implements Experiment {} /** See {@link #setMemoryCategoryInBackground(MemoryCategory)} */ public static final class MemoryCategoryInBackground implements Experiment { private final MemoryCategory memoryCategory; MemoryCategoryInBackground(MemoryCategory memoryCategory) { this.memoryCategory = memoryCategory; } public MemoryCategory value() { return memoryCategory; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/GlideContext.java ================================================ package com.bumptech.glide; import android.content.Context; import android.content.ContextWrapper; import android.widget.ImageView; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.Glide.RequestOptionsFactory; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.ImageViewTargetFactory; import com.bumptech.glide.request.target.ViewTarget; import com.bumptech.glide.util.GlideSuppliers; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * Global context for all loads in Glide containing and exposing the various registries and classes * required to load resources. */ @SuppressWarnings("PMD.DataClass") public class GlideContext extends ContextWrapper { @VisibleForTesting static final TransitionOptions DEFAULT_TRANSITION_OPTIONS = new GenericTransitionOptions<>(); private final ArrayPool arrayPool; private final GlideSupplier registry; private final ImageViewTargetFactory imageViewTargetFactory; private final RequestOptionsFactory defaultRequestOptionsFactory; private final List> defaultRequestListeners; private final Map, TransitionOptions> defaultTransitionOptions; private final Engine engine; private final GlideExperiments experiments; private final int logLevel; @Nullable @GuardedBy("this") private RequestOptions defaultRequestOptions; public GlideContext( @NonNull Context context, @NonNull ArrayPool arrayPool, @NonNull GlideSupplier registry, @NonNull ImageViewTargetFactory imageViewTargetFactory, @NonNull RequestOptionsFactory defaultRequestOptionsFactory, @NonNull Map, TransitionOptions> defaultTransitionOptions, @NonNull List> defaultRequestListeners, @NonNull Engine engine, @NonNull GlideExperiments experiments, int logLevel) { super(context.getApplicationContext()); this.arrayPool = arrayPool; this.imageViewTargetFactory = imageViewTargetFactory; this.defaultRequestOptionsFactory = defaultRequestOptionsFactory; this.defaultRequestListeners = defaultRequestListeners; this.defaultTransitionOptions = defaultTransitionOptions; this.engine = engine; this.experiments = experiments; this.logLevel = logLevel; this.registry = GlideSuppliers.memorize(registry); } public List> getDefaultRequestListeners() { return defaultRequestListeners; } public synchronized RequestOptions getDefaultRequestOptions() { if (defaultRequestOptions == null) { defaultRequestOptions = defaultRequestOptionsFactory.build().lock(); } return defaultRequestOptions; } @SuppressWarnings("unchecked") @NonNull public TransitionOptions getDefaultTransitionOptions(@NonNull Class transcodeClass) { TransitionOptions result = defaultTransitionOptions.get(transcodeClass); if (result == null) { for (Entry, TransitionOptions> value : defaultTransitionOptions.entrySet()) { if (value.getKey().isAssignableFrom(transcodeClass)) { result = value.getValue(); } } } if (result == null) { result = DEFAULT_TRANSITION_OPTIONS; } return (TransitionOptions) result; } @NonNull public ViewTarget buildImageViewTarget( @NonNull ImageView imageView, @NonNull Class transcodeClass) { return imageViewTargetFactory.buildTarget(imageView, transcodeClass); } @NonNull public Engine getEngine() { return engine; } @NonNull public Registry getRegistry() { return registry.get(); } public int getLogLevel() { return logLevel; } @NonNull public ArrayPool getArrayPool() { return arrayPool; } public GlideExperiments getExperiments() { return experiments; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/GlideExperiments.java ================================================ package com.bumptech.glide; import androidx.annotation.Nullable; import com.bumptech.glide.util.Synthetic; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * Keeps track of a set of Experimental features that may be enabled in Glide, simplifying the * process of adding and removing them. * *

    This is an experimental API, it may be removed at any point without deprecation or other * notice. */ // non-final for mocking public class GlideExperiments { private final Map, Experiment> experiments; @Synthetic GlideExperiments(Builder builder) { this.experiments = Collections.unmodifiableMap(new HashMap, Experiment>(builder.experiments)); } @SuppressWarnings("unchecked") @Nullable T get(Class clazz) { return (T) experiments.get(clazz); } /** * Returns {@code true} if the given experiment is enabled. * *

    This is an experimental API, it may be removed at any point without deprecation or other * notice. */ public boolean isEnabled(Class clazz) { return experiments.containsKey(clazz); } interface Experiment {} static final class Builder { private final Map, Experiment> experiments = new HashMap<>(); Builder update(Experiment experiment, boolean isEnabled) { if (isEnabled) { add(experiment); } else { experiments.remove(experiment.getClass()); } return this; } Builder add(Experiment experiment) { experiments.put(experiment.getClass(), experiment); return this; } GlideExperiments build() { return new GlideExperiments(this); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/ListPreloader.java ================================================ package com.bumptech.glide; import android.graphics.drawable.Drawable; import android.widget.AbsListView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.util.List; import java.util.Queue; /** * Loads a few resources ahead in the direction of scrolling in any {@link AbsListView} so that * images are in the memory cache just before the corresponding view in created in the list. Gives * the appearance of an infinitely large image cache, depending on scrolling speed, cpu speed, and * cache size. * *

    Must be put using {@link * AbsListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, or have its * corresponding methods called from another {@link android.widget.AbsListView.OnScrollListener} to * function. * * @param The type of the model being displayed in the list. */ public class ListPreloader implements AbsListView.OnScrollListener { private final int maxPreload; private final PreloadTargetQueue preloadTargetQueue; private final RequestManager requestManager; private final PreloadModelProvider preloadModelProvider; private final PreloadSizeProvider preloadDimensionProvider; private int lastEnd; private int lastStart; private int lastFirstVisible = -1; private int totalItemCount; private boolean isIncreasing = true; /** * An implementation of PreloadModelProvider should provide all the models that should be * preloaded. * * @param The type of the model being preloaded. */ public interface PreloadModelProvider { /** * Returns a {@link List} of models that need to be loaded for the list to display adapter items * in positions between {@code start} and {@code end}. * *

    {@code position} is the position in the view. If the view contains a mix of types (e.g. * headers and images) then not every view position will actually have any model to return here. * If that's the case for the given {@code position}, then return an empty list. * *

    A list of any size can be returned so there can be multiple models per adapter position. * *

    Every model returned by this method is expected to produce a valid {@link RequestBuilder} * in {@link #getPreloadRequestBuilder(Object)}. If that's not possible for any set of models, * avoid including them in the {@link List} returned by this method. * *

    Although it's acceptable for the returned {@link List} to contain {@code null} models, * it's best to filter them from the list instead of adding {@code null} to avoid unnecessary * logic and expanding the size of the {@link List} * * @param position The adapter position. */ @NonNull List getPreloadItems(int position); /** * Returns a {@link RequestBuilder} for a given item on which {@link * RequestBuilder#load(Object)}} has been called or {@code null} if no valid load can be * started. * *

    For the preloader to be effective, the {@link RequestBuilder} returned here must use * exactly the same size and set of options as the {@link RequestBuilder} used when the ``View`` * is bound. You may need to specify a size in both places to ensure that the width and height * match exactly. If so, you can use {@link * com.bumptech.glide.request.RequestOptions#override(int, int)} to do so. * *

    The target and context will be provided by the preloader. * *

    If {@link RequestBuilder#load(Object)} is not called by this method, the preloader will * trigger a {@link RuntimeException}. If you don't want to load a particular item or position, * filter it from the list returned by {@link #getPreloadItems(int)}. * * @param item The model to load. */ @Nullable RequestBuilder getPreloadRequestBuilder(@NonNull U item); } /** * An implementation of PreloadSizeProvider should provide the size of the view in the list where * the resources will be displayed. * * @param The type of the model the size should be provided for. */ public interface PreloadSizeProvider { /** * Returns the size of the view in the list where the resources will be displayed in pixels in * the format [x, y], or {@code null} if no size is currently available. * *

    Note - The dimensions returned here must precisely match those of the view in the list. * *

    If this method returns {@code null}, then no request will be started for the given item. * * @param item A model */ @Nullable int[] getPreloadSize(@NonNull T item, int adapterPosition, int perItemPosition); } /** * Constructor for {@link com.bumptech.glide.ListPreloader} that accepts interfaces for providing * the dimensions of images to preload, the list of models to preload for a given position, and * the request to use to load images. * * @param preloadModelProvider Provides models to load and requests capable of loading them. * @param preloadDimensionProvider Provides the dimensions of images to load. * @param maxPreload Maximum number of items to preload. */ public ListPreloader( @NonNull RequestManager requestManager, @NonNull PreloadModelProvider preloadModelProvider, @NonNull PreloadSizeProvider preloadDimensionProvider, int maxPreload) { this.requestManager = requestManager; this.preloadModelProvider = preloadModelProvider; this.preloadDimensionProvider = preloadDimensionProvider; this.maxPreload = maxPreload; preloadTargetQueue = new PreloadTargetQueue(maxPreload + 1); } @Override public void onScrollStateChanged(AbsListView absListView, int scrollState) { // Do nothing. } @Override public void onScroll( AbsListView absListView, int firstVisible, int visibleCount, int totalCount) { if (totalItemCount == 0 && totalCount == 0) { return; } totalItemCount = totalCount; if (firstVisible > lastFirstVisible) { preload(firstVisible + visibleCount, true); } else if (firstVisible < lastFirstVisible) { preload(firstVisible, false); } lastFirstVisible = firstVisible; } private void preload(int start, boolean increasing) { if (isIncreasing != increasing) { isIncreasing = increasing; cancelAll(); } preload(start, start + (increasing ? maxPreload : -maxPreload)); } private void preload(int from, int to) { int start; int end; if (from < to) { start = Math.max(lastEnd, from); end = to; } else { start = to; end = Math.min(lastStart, from); } end = Math.min(totalItemCount, end); start = Math.min(totalItemCount, Math.max(0, start)); if (from < to) { // Increasing for (int i = start; i < end; i++) { preloadAdapterPosition( preloadModelProvider.getPreloadItems(i), /* position= */ i, /* isIncreasing= */ true); } } else { // Decreasing for (int i = end - 1; i >= start; i--) { preloadAdapterPosition( preloadModelProvider.getPreloadItems(i), /* position= */ i, /* isIncreasing= */ false); } } lastStart = start; lastEnd = end; } private void preloadAdapterPosition(List items, int position, boolean isIncreasing) { final int numItems = items.size(); if (isIncreasing) { for (int i = 0; i < numItems; ++i) { preloadItem(items.get(i), position, i); } } else { for (int i = numItems - 1; i >= 0; --i) { preloadItem(items.get(i), position, i); } } } @SuppressWarnings("unchecked") private void preloadItem(@Nullable T item, int position, int perItemPosition) { if (item == null) { return; } int[] dimensions = preloadDimensionProvider.getPreloadSize(item, position, perItemPosition); if (dimensions == null) { return; } RequestBuilder preloadRequestBuilder = (RequestBuilder) preloadModelProvider.getPreloadRequestBuilder(item); if (preloadRequestBuilder == null) { return; } preloadRequestBuilder.into(preloadTargetQueue.next(dimensions[0], dimensions[1])); } private void cancelAll() { for (int i = 0; i < preloadTargetQueue.queue.size(); i++) { requestManager.clear(preloadTargetQueue.next(0, 0)); } } private static final class PreloadTargetQueue { @Synthetic final Queue queue; // The loop is short and the only point is to create the objects. @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") PreloadTargetQueue(int size) { queue = Util.createQueue(size); for (int i = 0; i < size; i++) { queue.offer(new PreloadTarget()); } } public PreloadTarget next(int width, int height) { final PreloadTarget result = queue.poll(); queue.offer(result); result.photoWidth = width; result.photoHeight = height; return result; } } private static final class PreloadTarget implements Target { @Synthetic int photoHeight; @Synthetic int photoWidth; @Nullable private Request request; @Synthetic PreloadTarget() {} @Override public void onLoadStarted(@Nullable Drawable placeholder) { // Do nothing. } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { // Do nothing. } @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) { // Do nothing. } @Override public void onLoadCleared(@Nullable Drawable placeholder) { // Do nothing. } @Override public void getSize(@NonNull SizeReadyCallback cb) { cb.onSizeReady(photoWidth, photoHeight); } @Override public void removeCallback(@NonNull SizeReadyCallback cb) { // Do nothing because we don't retain references to SizeReadyCallbacks. } @Override public void setRequest(@Nullable Request request) { this.request = request; } @Nullable @Override public Request getRequest() { return request; } @Override public void onStart() { // Do nothing. } @Override public void onStop() { // Do nothing. } @Override public void onDestroy() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/MemoryCategory.java ================================================ package com.bumptech.glide; /** An enum for dynamically modifying the amount of memory Glide is able to use. */ public enum MemoryCategory { /** Tells Glide's memory cache and bitmap pool to use no memory. */ ZERO(0f), /** * Tells Glide's memory cache and bitmap pool to use at most half of their initial maximum size. */ LOW(0.5f), /** Tells Glide's memory cache and bitmap pool to use at most their initial maximum size. */ NORMAL(1f), /** * Tells Glide's memory cache and bitmap pool to use at most one and a half times their initial * maximum size. */ HIGH(1.5f); private final float multiplier; MemoryCategory(float multiplier) { this.multiplier = multiplier; } /** * Returns the multiplier that should be applied to the initial maximum size of Glide's memory * cache and bitmap pool. */ public float getMultiplier() { return multiplier; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/ModelTypes.java ================================================ package com.bumptech.glide; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import java.io.File; import java.net.URL; /** * Ensures that the set of explicitly supported model types remains consistent across Glide's API * surface. */ interface ModelTypes { @NonNull @CheckResult T load(@Nullable Bitmap bitmap); @NonNull @CheckResult T load(@Nullable Drawable drawable); @NonNull @CheckResult T load(@Nullable String string); @NonNull @CheckResult T load(@Nullable Uri uri); @NonNull @CheckResult T load(@Nullable File file); @NonNull @CheckResult T load(@RawRes @DrawableRes @Nullable Integer resourceId); @Deprecated @CheckResult T load(@Nullable URL url); @NonNull @CheckResult T load(@Nullable byte[] model); @NonNull @CheckResult @SuppressWarnings("unchecked") T load(@Nullable Object model); } ================================================ FILE: library/src/main/java/com/bumptech/glide/Priority.java ================================================ package com.bumptech.glide; /** * Priorities for completing loads. If more than one load is queued at a time, the load with the * higher priority will be started first. Priorities are considered best effort, there are no * guarantees about the order in which loads will start or finish. */ public enum Priority { IMMEDIATE, HIGH, NORMAL, LOW, } ================================================ FILE: library/src/main/java/com/bumptech/glide/Registry.java ================================================ package com.bumptech.glide; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pools.Pool; import com.bumptech.glide.load.Encoder; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.load.data.DataRewinderRegistry; import com.bumptech.glide.load.engine.DecodePath; import com.bumptech.glide.load.engine.LoadPath; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.ModelLoaderRegistry; import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; import com.bumptech.glide.load.resource.transcode.TranscoderRegistry; import com.bumptech.glide.provider.EncoderRegistry; import com.bumptech.glide.provider.ImageHeaderParserRegistry; import com.bumptech.glide.provider.LoadPathCache; import com.bumptech.glide.provider.ModelToResourceClassCache; import com.bumptech.glide.provider.ResourceDecoderRegistry; import com.bumptech.glide.provider.ResourceEncoderRegistry; import com.bumptech.glide.util.pool.FactoryPools; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Manages component registration to extend or replace Glide's default loading, decoding, and * encoding logic. */ // Public API. @SuppressWarnings({"WeakerAccess", "unused"}) public class Registry { public static final String BUCKET_ANIMATION = "Animation"; /** * @deprecated Identical to {@link #BUCKET_ANIMATION}, just with a more confusing name. This * bucket can be used for all animation types (including webp). */ @Deprecated public static final String BUCKET_GIF = BUCKET_ANIMATION; public static final String BUCKET_BITMAP = "Bitmap"; public static final String BUCKET_BITMAP_DRAWABLE = "BitmapDrawable"; private static final String BUCKET_PREPEND_ALL = "legacy_prepend_all"; private static final String BUCKET_APPEND_ALL = "legacy_append"; private final ModelLoaderRegistry modelLoaderRegistry; private final EncoderRegistry encoderRegistry; private final ResourceDecoderRegistry decoderRegistry; private final ResourceEncoderRegistry resourceEncoderRegistry; private final DataRewinderRegistry dataRewinderRegistry; private final TranscoderRegistry transcoderRegistry; private final ImageHeaderParserRegistry imageHeaderParserRegistry; private final ModelToResourceClassCache modelToResourceClassCache = new ModelToResourceClassCache(); private final LoadPathCache loadPathCache = new LoadPathCache(); private final Pool> throwableListPool = FactoryPools.threadSafeList(); public Registry() { this.modelLoaderRegistry = new ModelLoaderRegistry(throwableListPool); this.encoderRegistry = new EncoderRegistry(); this.decoderRegistry = new ResourceDecoderRegistry(); this.resourceEncoderRegistry = new ResourceEncoderRegistry(); this.dataRewinderRegistry = new DataRewinderRegistry(); this.transcoderRegistry = new TranscoderRegistry(); this.imageHeaderParserRegistry = new ImageHeaderParserRegistry(); setResourceDecoderBucketPriorityList( Arrays.asList(BUCKET_ANIMATION, BUCKET_BITMAP, BUCKET_BITMAP_DRAWABLE)); } /** * Registers the given {@link Encoder} for the given data class (InputStream, FileDescriptor etc). * *

    The {@link Encoder} will be used both for the exact data class and any subtypes. For * example, registering an {@link Encoder} for {@link java.io.InputStream} will result in the * {@link Encoder} being used for {@link * android.content.res.AssetFileDescriptor.AutoCloseInputStream}, {@link java.io.FileInputStream} * and any other subclass. * *

    If multiple {@link Encoder}s are registered for the same type or super type, the {@link * Encoder} that is registered first will be used. * * @deprecated Use the equivalent {@link #append(Class, Class, ModelLoaderFactory)} method * instead. */ @NonNull @Deprecated public Registry register(@NonNull Class dataClass, @NonNull Encoder encoder) { return append(dataClass, encoder); } /** * Appends the given {@link Encoder} onto the list of available {@link Encoder}s so that it is * attempted after all earlier and default {@link Encoder}s for the given data class. * *

    The {@link Encoder} will be used both for the exact data class and any subtypes. For * example, registering an {@link Encoder} for {@link java.io.InputStream} will result in the * {@link Encoder} being used for {@link * android.content.res.AssetFileDescriptor.AutoCloseInputStream}, {@link java.io.FileInputStream} * and any other subclass. * *

    If multiple {@link Encoder}s are registered for the same type or super type, the {@link * Encoder} that is registered first will be used. * * @see #prepend(Class, Encoder) */ @NonNull public Registry append(@NonNull Class dataClass, @NonNull Encoder encoder) { encoderRegistry.append(dataClass, encoder); return this; } /** * Prepends the given {@link Encoder} into the list of available {@link Encoder}s so that it is * attempted before all later and default {@link Encoder}s for the given data class. * *

    This method allows you to replace the default {@link Encoder} because it ensures the * registered {@link Encoder} will run first. If multiple {@link Encoder}s are registered for the * same type or super type, the {@link Encoder} that is registered first will be used. * * @see #append(Class, Encoder) */ @NonNull public Registry prepend(@NonNull Class dataClass, @NonNull Encoder encoder) { encoderRegistry.prepend(dataClass, encoder); return this; } /** * Appends the given {@link ResourceDecoder} onto the list of all available {@link * ResourceDecoder}s allowing it to be used if all earlier and default {@link ResourceDecoder}s * for the given types fail (or there are none). * *

    If you're attempting to replace an existing {@link ResourceDecoder} or would like to ensure * that your {@link ResourceDecoder} gets the chance to run before an existing {@link * ResourceDecoder}, use {@link #prepend(Class, Class, ResourceDecoder)}. This method is best for * new types of resources and data or as a way to add an additional fallback decoder for an * existing type of data. * * @see #append(String, Class, Class, ResourceDecoder) * @see #prepend(Class, Class, ResourceDecoder) * @param dataClass The data that will be decoded from ({@link java.io.InputStream}, {@link * java.io.FileDescriptor} etc). * @param resourceClass The resource that will be decoded to ({@link android.graphics.Bitmap}, * {@link com.bumptech.glide.load.resource.gif.GifDrawable} etc). * @param decoder The {@link ResourceDecoder} to register. */ @NonNull public Registry append( @NonNull Class dataClass, @NonNull Class resourceClass, @NonNull ResourceDecoder decoder) { append(BUCKET_APPEND_ALL, dataClass, resourceClass, decoder); return this; } /** * Appends the given {@link ResourceDecoder} onto the list of available {@link ResourceDecoder}s * in this bucket, allowing it to be used if all earlier and default {@link ResourceDecoder}s for * the given types in this bucket fail (or there are none). * *

    If you're attempting to replace an existing {@link ResourceDecoder} or would like to ensure * that your {@link ResourceDecoder} gets the chance to run before an existing {@link * ResourceDecoder}, use {@link #prepend(Class, Class, ResourceDecoder)}. This method is best for * new types of resources and data or as a way to add an additional fallback decoder for an * existing type of data. * * @see #prepend(String, Class, Class, ResourceDecoder) * @see #setResourceDecoderBucketPriorityList(List) * @param bucket The bucket identifier to add this decoder to. * @param dataClass The data that will be decoded from ({@link java.io.InputStream}, {@link * java.io.FileDescriptor} etc). * @param resourceClass The resource that will be decoded to ({@link android.graphics.Bitmap}, * {@link com.bumptech.glide.load.resource.gif.GifDrawable} etc). * @param decoder The {@link ResourceDecoder} to register. */ @NonNull public Registry append( @NonNull String bucket, @NonNull Class dataClass, @NonNull Class resourceClass, @NonNull ResourceDecoder decoder) { decoderRegistry.append(bucket, decoder, dataClass, resourceClass); return this; } /** * Prepends the given {@link ResourceDecoder} into the list of all available {@link * ResourceDecoder}s so that it is attempted before all later and default {@link ResourceDecoder}s * for the given types. * *

    This method allows you to replace the default {@link ResourceDecoder} because it ensures the * registered {@link ResourceDecoder} will run first. You can use the {@link * ResourceDecoder#handles(Object, Options)} to fall back to the default {@link ResourceDecoder}s * if you only want to change the default functionality for certain types of data. * * @see #prepend(String, Class, Class, ResourceDecoder) * @see #append(Class, Class, ResourceDecoder) * @param dataClass The data that will be decoded from ({@link java.io.InputStream}, {@link * java.io.FileDescriptor} etc). * @param resourceClass The resource that will be decoded to ({@link android.graphics.Bitmap}, * {@link com.bumptech.glide.load.resource.gif.GifDrawable} etc). * @param decoder The {@link ResourceDecoder} to register. */ @NonNull public Registry prepend( @NonNull Class dataClass, @NonNull Class resourceClass, @NonNull ResourceDecoder decoder) { prepend(BUCKET_PREPEND_ALL, dataClass, resourceClass, decoder); return this; } /** * Prepends the given {@link ResourceDecoder} into the list of available {@link ResourceDecoder}s * in the same bucket so that it is attempted before all later and default {@link * ResourceDecoder}s for the given types in that bucket. * *

    This method allows you to replace the default {@link ResourceDecoder} for this bucket * because it ensures the registered {@link ResourceDecoder} will run first. You can use the * {@link ResourceDecoder#handles(Object, Options)} to fall back to the default {@link * ResourceDecoder}s if you only want to change the default functionality for certain types of * data. * * @see #append(String, Class, Class, ResourceDecoder) * @see #setResourceDecoderBucketPriorityList(List) * @param bucket The bucket identifier to add this decoder to. * @param dataClass The data that will be decoded from ({@link java.io.InputStream}, {@link * java.io.FileDescriptor} etc). * @param resourceClass The resource that will be decoded to ({@link android.graphics.Bitmap}, * {@link com.bumptech.glide.load.resource.gif.GifDrawable} etc). * @param decoder The {@link ResourceDecoder} to register. */ @NonNull public Registry prepend( @NonNull String bucket, @NonNull Class dataClass, @NonNull Class resourceClass, @NonNull ResourceDecoder decoder) { decoderRegistry.prepend(bucket, decoder, dataClass, resourceClass); return this; } /** * Overrides the default ordering of resource decoder buckets. You may also add custom buckets * which are identified as a unique string. Glide will attempt to decode using decoders in the * highest priority bucket before moving on to the next one. * *

    The default order is [{@link #BUCKET_ANIMATION}, {@link #BUCKET_BITMAP}, {@link * #BUCKET_BITMAP_DRAWABLE}]. * *

    When registering decoders, you can use these buckets to specify the ordering relative only * to other decoders in that bucket. * * @see #append(String, Class, Class, ResourceDecoder) * @see #prepend(String, Class, Class, ResourceDecoder) * @param buckets The list of bucket identifiers in order from highest priority to least priority. */ // Final to avoid a PMD error. @NonNull public final Registry setResourceDecoderBucketPriorityList(@NonNull List buckets) { // See #3296 and https://bugs.openjdk.java.net/browse/JDK-6260652. List modifiedBuckets = new ArrayList<>(buckets.size()); modifiedBuckets.add(BUCKET_PREPEND_ALL); // See https://github.com/bumptech/glide/issues/4309. for (String bucket : buckets) { modifiedBuckets.add(bucket); } modifiedBuckets.add(BUCKET_APPEND_ALL); decoderRegistry.setBucketPriorityList(modifiedBuckets); return this; } /** * Appends the given {@link ResourceEncoder} into the list of available {@link ResourceEncoder}s * so that it is attempted after all earlier and default {@link ResourceEncoder}s for the given * data type. * *

    The {@link ResourceEncoder} will be used both for the exact resource class and any subtypes. * For example, registering an {@link ResourceEncoder} for {@link * android.graphics.drawable.Drawable} (not recommended) will result in the {@link * ResourceEncoder} being used for {@link android.graphics.drawable.BitmapDrawable} and {@link * com.bumptech.glide.load.resource.gif.GifDrawable} and any other subclass. * *

    If multiple {@link ResourceEncoder}s are registered for the same type or super type, the * {@link ResourceEncoder} that is registered first will be used. * * @deprecated Use the equivalent {@link #append(Class, ResourceEncoder)} method instead. */ @NonNull @Deprecated public Registry register( @NonNull Class resourceClass, @NonNull ResourceEncoder encoder) { return append(resourceClass, encoder); } /** * Appends the given {@link ResourceEncoder} into the list of available {@link ResourceEncoder}s * so that it is attempted after all earlier and default {@link ResourceEncoder}s for the given * data type. * *

    The {@link ResourceEncoder} will be used both for the exact resource class and any subtypes. * For example, registering an {@link ResourceEncoder} for {@link * android.graphics.drawable.Drawable} (not recommended) will result in the {@link * ResourceEncoder} being used for {@link android.graphics.drawable.BitmapDrawable} and {@link * com.bumptech.glide.load.resource.gif.GifDrawable} and any other subclass. * *

    If multiple {@link ResourceEncoder}s are registered for the same type or super type, the * {@link ResourceEncoder} that is registered first will be used. * * @see #prepend(Class, ResourceEncoder) */ @NonNull public Registry append( @NonNull Class resourceClass, @NonNull ResourceEncoder encoder) { resourceEncoderRegistry.append(resourceClass, encoder); return this; } /** * Prepends the given {@link ResourceEncoder} into the list of available {@link ResourceEncoder}s * so that it is attempted before all later and default {@link ResourceEncoder}s for the given * data type. * *

    This method allows you to replace the default {@link ResourceEncoder} because it ensures the * registered {@link ResourceEncoder} will run first. If multiple {@link ResourceEncoder}s are * registered for the same type or super type, the {@link ResourceEncoder} that is registered * first will be used. * * @see #append(Class, ResourceEncoder) */ @NonNull public Registry prepend( @NonNull Class resourceClass, @NonNull ResourceEncoder encoder) { resourceEncoderRegistry.prepend(resourceClass, encoder); return this; } /** * Registers a new {@link com.bumptech.glide.load.data.DataRewinder.Factory} to handle a * non-default data type that can be rewind to allow for efficient reads of file headers. */ @NonNull public Registry register(@NonNull DataRewinder.Factory factory) { dataRewinderRegistry.register(factory); return this; } /** * Registers the given {@link ResourceTranscoder} to convert from the given resource {@link Class} * to the given transcode {@link Class}. * * @param resourceClass The class that will be transcoded from (e.g. {@link * android.graphics.Bitmap}). * @param transcodeClass The class that will be transcoded to (e.g. {@link * android.graphics.drawable.BitmapDrawable}). * @param transcoder The {@link ResourceTranscoder} to register. */ @NonNull public Registry register( @NonNull Class resourceClass, @NonNull Class transcodeClass, @NonNull ResourceTranscoder transcoder) { transcoderRegistry.register(resourceClass, transcodeClass, transcoder); return this; } /** * Registers a new {@link ImageHeaderParser} that can obtain some basic metadata from an image * header (orientation, type etc). */ @NonNull public Registry register(@NonNull ImageHeaderParser parser) { imageHeaderParserRegistry.add(parser); return this; } /** * Appends a new {@link ModelLoaderFactory} onto the end of the existing set so that the * constructed {@link ModelLoader} will be tried after all default and previously registered * {@link ModelLoader}s for the given model and data classes. * *

    If you're attempting to replace an existing {@link ModelLoader}, use {@link #prepend(Class, * Class, ModelLoaderFactory)}. This method is best for new types of models and/or data or as a * way to add an additional fallback loader for an existing type of model/data. * *

    If multiple {@link ModelLoaderFactory}s are registered for the same model and/or data * classes, the {@link ModelLoader}s they produce will be attempted in the order the {@link * ModelLoaderFactory}s were registered. Only if all {@link ModelLoader}s fail will the entire * request fail. * * @see #prepend(Class, Class, ModelLoaderFactory) * @see #replace(Class, Class, ModelLoaderFactory) * @param modelClass The model class (e.g. URL, file path). * @param dataClass the data class (e.g. {@link java.io.InputStream}, {@link * java.io.FileDescriptor}). */ @NonNull public Registry append( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { modelLoaderRegistry.append(modelClass, dataClass, factory); return this; } /** * Prepends a new {@link ModelLoaderFactory} onto the beginning of the existing set so that the * constructed {@link ModelLoader} will be tried before all default and previously registered * {@link ModelLoader}s for the given model and data classes. * *

    If you're attempting to add additional functionality or add a backup that should run only * after the default {@link ModelLoader}s run, use {@link #append(Class, Class, * ModelLoaderFactory)}. This method is best for adding an additional case to Glide's existing * functionality that should run first. This method will still run Glide's default {@link * ModelLoader}s if the prepended {@link ModelLoader}s fail. * *

    If multiple {@link ModelLoaderFactory}s are registered for the same model and/or data * classes, the {@link ModelLoader}s they produce will be attempted in the order the {@link * ModelLoaderFactory}s were registered. Only if all {@link ModelLoader}s fail will the entire * request fail. * * @see #append(Class, Class, ModelLoaderFactory) * @see #replace(Class, Class, ModelLoaderFactory) * @param modelClass The model class (e.g. URL, file path). * @param dataClass the data class (e.g. {@link java.io.InputStream}, {@link * java.io.FileDescriptor}). */ @NonNull public Registry prepend( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { modelLoaderRegistry.prepend(modelClass, dataClass, factory); return this; } /** * Removes all default and previously registered {@link ModelLoaderFactory}s for the given data * and model class and replaces all of them with the single {@link ModelLoader} provided. * *

    If you're attempting to add additional functionality or add a backup that should run only * after the default {@link ModelLoader}s run, use {@link #append(Class, Class, * ModelLoaderFactory)}. This method should be used only when you want to ensure that Glide's * default {@link ModelLoader}s do not run. * *

    One good use case for this method is when you want to replace Glide's default networking * library with your OkHttp, Volley, or your own implementation. Using {@link #prepend(Class, * Class, ModelLoaderFactory)} or {@link #append(Class, Class, ModelLoaderFactory)} may still * allow Glide's default networking library to run in some cases. Using this method will ensure * that only your networking library will run and that the request will fail otherwise. * * @see #prepend(Class, Class, ModelLoaderFactory) * @see #append(Class, Class, ModelLoaderFactory) * @param modelClass The model class (e.g. URL, file path). * @param dataClass the data class (e.g. {@link java.io.InputStream}, {@link * java.io.FileDescriptor}). */ @NonNull public Registry replace( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { modelLoaderRegistry.replace(modelClass, dataClass, factory); return this; } @Nullable public LoadPath getLoadPath( @NonNull Class dataClass, @NonNull Class resourceClass, @NonNull Class transcodeClass) { LoadPath result = loadPathCache.get(dataClass, resourceClass, transcodeClass); if (loadPathCache.isEmptyLoadPath(result)) { return null; } else if (result == null) { List> decodePaths = getDecodePaths(dataClass, resourceClass, transcodeClass); // It's possible there is no way to decode or transcode to the desired types from a given // data class. if (decodePaths.isEmpty()) { result = null; } else { result = new LoadPath<>( dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool); } loadPathCache.put(dataClass, resourceClass, transcodeClass, result); } return result; } @NonNull private List> getDecodePaths( @NonNull Class dataClass, @NonNull Class resourceClass, @NonNull Class transcodeClass) { List> decodePaths = new ArrayList<>(); List> registeredResourceClasses = decoderRegistry.getResourceClasses(dataClass, resourceClass); for (Class registeredResourceClass : registeredResourceClasses) { List> registeredTranscodeClasses = transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass); for (Class registeredTranscodeClass : registeredTranscodeClasses) { List> decoders = decoderRegistry.getDecoders(dataClass, registeredResourceClass); ResourceTranscoder transcoder = transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass); @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") DecodePath path = new DecodePath<>( dataClass, registeredResourceClass, registeredTranscodeClass, decoders, transcoder, throwableListPool); decodePaths.add(path); } } return decodePaths; } @NonNull public List> getRegisteredResourceClasses( @NonNull Class modelClass, @NonNull Class resourceClass, @NonNull Class transcodeClass) { List> result = modelToResourceClassCache.get(modelClass, resourceClass, transcodeClass); if (result == null) { result = new ArrayList<>(); List> dataClasses = modelLoaderRegistry.getDataClasses(modelClass); for (Class dataClass : dataClasses) { List> registeredResourceClasses = decoderRegistry.getResourceClasses(dataClass, resourceClass); for (Class registeredResourceClass : registeredResourceClasses) { List> registeredTranscodeClasses = transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass); if (!registeredTranscodeClasses.isEmpty() && !result.contains(registeredResourceClass)) { result.add(registeredResourceClass); } } } modelToResourceClassCache.put( modelClass, resourceClass, transcodeClass, Collections.unmodifiableList(result)); } return result; } public boolean isResourceEncoderAvailable(@NonNull Resource resource) { return resourceEncoderRegistry.get(resource.getResourceClass()) != null; } @NonNull public ResourceEncoder getResultEncoder(@NonNull Resource resource) throws NoResultEncoderAvailableException { ResourceEncoder resourceEncoder = resourceEncoderRegistry.get(resource.getResourceClass()); if (resourceEncoder != null) { return resourceEncoder; } throw new NoResultEncoderAvailableException(resource.getResourceClass()); } @NonNull @SuppressWarnings("unchecked") public Encoder getSourceEncoder(@NonNull X data) throws NoSourceEncoderAvailableException { Encoder encoder = encoderRegistry.getEncoder((Class) data.getClass()); if (encoder != null) { return encoder; } throw new NoSourceEncoderAvailableException(data.getClass()); } @NonNull public DataRewinder getRewinder(@NonNull X data) { return dataRewinderRegistry.build(data); } @NonNull public List> getModelLoaders(@NonNull Model model) { return modelLoaderRegistry.getModelLoaders(model); } @NonNull public List getImageHeaderParsers() { List result = imageHeaderParserRegistry.getParsers(); if (result.isEmpty()) { throw new NoImageHeaderParserException(); } return result; } /** * Thrown when no {@link com.bumptech.glide.load.model.ModelLoader} is registered for a given * model class. */ // Never serialized by Glide. @SuppressWarnings("serial") public static class NoModelLoaderAvailableException extends MissingComponentException { public NoModelLoaderAvailableException(@NonNull Object model) { super("Failed to find any ModelLoaders registered for model class: " + model.getClass()); } public NoModelLoaderAvailableException( @NonNull M model, @NonNull List> matchingButNotHandlingModelLoaders) { super( "Found ModelLoaders for model class: " + matchingButNotHandlingModelLoaders + ", but none that handle this specific model instance: " + model); } public NoModelLoaderAvailableException( @NonNull Class modelClass, @NonNull Class dataClass) { super("Failed to find any ModelLoaders for model: " + modelClass + " and data: " + dataClass); } } /** Thrown when no {@link ResourceEncoder} is registered for a given resource class. */ // Never serialized by Glide. @SuppressWarnings("serial") public static class NoResultEncoderAvailableException extends MissingComponentException { public NoResultEncoderAvailableException(@NonNull Class resourceClass) { super( "Failed to find result encoder for resource class: " + resourceClass + ", you may need to consider registering a new Encoder for the requested type or" + " DiskCacheStrategy.DATA/DiskCacheStrategy.NONE if caching your transformed" + " resource is unnecessary."); } } /** Thrown when no {@link Encoder} is registered for a given data class. */ // Never serialized by Glide. @SuppressWarnings("serial") public static class NoSourceEncoderAvailableException extends MissingComponentException { public NoSourceEncoderAvailableException(@NonNull Class dataClass) { super("Failed to find source encoder for data class: " + dataClass); } } /** Thrown when some necessary component is missing for a load. */ // Never serialized by Glide. @SuppressWarnings("serial") public static class MissingComponentException extends RuntimeException { public MissingComponentException(@NonNull String message) { super(message); } } /** Thrown when no {@link ImageHeaderParser} is registered. */ // Never serialized by Glide. @SuppressWarnings("serial") public static final class NoImageHeaderParserException extends MissingComponentException { public NoImageHeaderParserException() { super("Failed to find image header parser."); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/RegistryFactory.java ================================================ package com.bumptech.glide; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; import androidx.annotation.Nullable; import androidx.tracing.Trace; import com.bumptech.glide.GlideBuilder.EnableImageDecoderForBitmaps; import com.bumptech.glide.GlideBuilder.UseMediaStoreOpenFileApisIfPossible; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.data.InputStreamRewinder; import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.model.AssetUriLoader; import com.bumptech.glide.load.model.ByteArrayLoader; import com.bumptech.glide.load.model.ByteBufferEncoder; import com.bumptech.glide.load.model.ByteBufferFileLoader; import com.bumptech.glide.load.model.DataUrlLoader; import com.bumptech.glide.load.model.DirectResourceLoader; import com.bumptech.glide.load.model.FileLoader; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.MediaStoreFileLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.ResourceLoader; import com.bumptech.glide.load.model.ResourceUriLoader; import com.bumptech.glide.load.model.StreamEncoder; import com.bumptech.glide.load.model.StringLoader; import com.bumptech.glide.load.model.UnitModelLoader; import com.bumptech.glide.load.model.UriLoader; import com.bumptech.glide.load.model.UrlUriLoader; import com.bumptech.glide.load.model.stream.HttpGlideUrlLoader; import com.bumptech.glide.load.model.stream.MediaStoreImageThumbLoader; import com.bumptech.glide.load.model.stream.MediaStoreVideoThumbLoader; import com.bumptech.glide.load.model.stream.QMediaStoreUriLoader; import com.bumptech.glide.load.model.stream.UrlLoader; import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder; import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder; import com.bumptech.glide.load.resource.bitmap.BitmapEncoder; import com.bumptech.glide.load.resource.bitmap.ByteBufferBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.ByteBufferBitmapImageDecoderResourceDecoder; import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser; import com.bumptech.glide.load.resource.bitmap.InputStreamBitmapImageDecoderResourceDecoder; import com.bumptech.glide.load.resource.bitmap.ParcelFileDescriptorBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.ResourceBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.UnitBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.VideoDecoder; import com.bumptech.glide.load.resource.bytes.ByteBufferRewinder; import com.bumptech.glide.load.resource.drawable.AnimatedImageDecoder; import com.bumptech.glide.load.resource.drawable.ResourceDrawableDecoder; import com.bumptech.glide.load.resource.drawable.UnitDrawableDecoder; import com.bumptech.glide.load.resource.file.FileDecoder; import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.load.resource.gif.GifDrawableEncoder; import com.bumptech.glide.load.resource.gif.GifFrameResourceDecoder; import com.bumptech.glide.load.resource.gif.StreamGifDecoder; import com.bumptech.glide.load.resource.transcode.BitmapBytesTranscoder; import com.bumptech.glide.load.resource.transcode.BitmapDrawableTranscoder; import com.bumptech.glide.load.resource.transcode.DrawableBytesTranscoder; import com.bumptech.glide.load.resource.transcode.GifDrawableBytesTranscoder; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.GlideModule; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import com.bumptech.glide.util.Synthetic; import java.io.File; import java.io.InputStream; import java.net.URL; import java.nio.ByteBuffer; import java.util.List; final class RegistryFactory { private RegistryFactory() {} static GlideSupplier lazilyCreateAndInitializeRegistry( final Glide glide, final List manifestModules, @Nullable final AppGlideModule annotationGeneratedModule) { return new GlideSupplier() { // Rely on callers using memoization if they want to avoid duplicate work, but // rely on ourselves to verify that no recursive initialization occurs. private boolean isInitializing; @Override public Registry get() { if (isInitializing) { throw new IllegalStateException( "Recursive Registry initialization! In your" + " AppGlideModule and LibraryGlideModules, Make sure you're using the provided " + "Registry rather calling glide.getRegistry()!"); } Trace.beginSection("Glide registry"); isInitializing = true; try { return createAndInitRegistry(glide, manifestModules, annotationGeneratedModule); } finally { isInitializing = false; Trace.endSection(); } } }; } @Synthetic static Registry createAndInitRegistry( Glide glide, List manifestModules, @Nullable AppGlideModule annotationGeneratedModule) { BitmapPool bitmapPool = glide.getBitmapPool(); ArrayPool arrayPool = glide.getArrayPool(); Context context = glide.getGlideContext().getApplicationContext(); GlideExperiments experiments = glide.getGlideContext().getExperiments(); Registry registry = new Registry(); initializeDefaults(context, registry, bitmapPool, arrayPool, experiments); initializeModules(context, glide, registry, manifestModules, annotationGeneratedModule); return registry; } private static void initializeDefaults( Context context, Registry registry, BitmapPool bitmapPool, ArrayPool arrayPool, GlideExperiments experiments) { registry.register(new DefaultImageHeaderParser()); // Right now we're only using this parser for HEIF images, which are only supported on OMR1+. // If we need this for other file types, we should consider removing this restriction. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { registry.register(new ExifInterfaceImageHeaderParser()); } final Resources resources = context.getResources(); List imageHeaderParsers = registry.getImageHeaderParsers(); ByteBufferGifDecoder byteBufferGifDecoder = new ByteBufferGifDecoder(context, imageHeaderParsers, bitmapPool, arrayPool); ResourceDecoder parcelFileDescriptorVideoDecoder = VideoDecoder.parcel(bitmapPool); // TODO(judds): Make ParcelFileDescriptorBitmapDecoder work with ImageDecoder. Downsampler downsampler = new Downsampler( registry.getImageHeaderParsers(), resources.getDisplayMetrics(), bitmapPool, arrayPool); ResourceDecoder byteBufferBitmapDecoder; ResourceDecoder streamBitmapDecoder; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && experiments.isEnabled(EnableImageDecoderForBitmaps.class)) { streamBitmapDecoder = new InputStreamBitmapImageDecoderResourceDecoder(); byteBufferBitmapDecoder = new ByteBufferBitmapImageDecoderResourceDecoder(); } else { byteBufferBitmapDecoder = new ByteBufferBitmapDecoder(downsampler); streamBitmapDecoder = new StreamBitmapDecoder(downsampler, arrayPool); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { registry.append( Registry.BUCKET_ANIMATION, InputStream.class, Drawable.class, AnimatedImageDecoder.streamDecoder(imageHeaderParsers, arrayPool)); registry.append( Registry.BUCKET_ANIMATION, ByteBuffer.class, Drawable.class, AnimatedImageDecoder.byteBufferDecoder(imageHeaderParsers, arrayPool)); } ResourceDrawableDecoder resourceDrawableDecoder = new ResourceDrawableDecoder(context); BitmapEncoder bitmapEncoder = new BitmapEncoder(arrayPool); BitmapBytesTranscoder bitmapBytesTranscoder = new BitmapBytesTranscoder(); GifDrawableBytesTranscoder gifDrawableBytesTranscoder = new GifDrawableBytesTranscoder(); ContentResolver contentResolver = context.getContentResolver(); registry .append(ByteBuffer.class, new ByteBufferEncoder()) .append(InputStream.class, new StreamEncoder(arrayPool)) /* Bitmaps */ .append(Registry.BUCKET_BITMAP, ByteBuffer.class, Bitmap.class, byteBufferBitmapDecoder) .append(Registry.BUCKET_BITMAP, InputStream.class, Bitmap.class, streamBitmapDecoder); if (ParcelFileDescriptorRewinder.isSupported()) { registry.append( Registry.BUCKET_BITMAP, ParcelFileDescriptor.class, Bitmap.class, new ParcelFileDescriptorBitmapDecoder(downsampler)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { registry.append( Registry.BUCKET_BITMAP, AssetFileDescriptor.class, Bitmap.class, VideoDecoder.asset(bitmapPool)); } registry .append( Registry.BUCKET_BITMAP, ParcelFileDescriptor.class, Bitmap.class, parcelFileDescriptorVideoDecoder) .append(Bitmap.class, Bitmap.class, UnitModelLoader.Factory.getInstance()) .append(Registry.BUCKET_BITMAP, Bitmap.class, Bitmap.class, new UnitBitmapDecoder()) .append(Bitmap.class, bitmapEncoder) /* BitmapDrawables */ .append( Registry.BUCKET_BITMAP_DRAWABLE, ByteBuffer.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, byteBufferBitmapDecoder)) .append( Registry.BUCKET_BITMAP_DRAWABLE, InputStream.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, streamBitmapDecoder)) .append( Registry.BUCKET_BITMAP_DRAWABLE, ParcelFileDescriptor.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, parcelFileDescriptorVideoDecoder)) .append(BitmapDrawable.class, new BitmapDrawableEncoder(bitmapPool, bitmapEncoder)) /* GIFs */ .append( Registry.BUCKET_ANIMATION, InputStream.class, GifDrawable.class, new StreamGifDecoder(imageHeaderParsers, byteBufferGifDecoder, arrayPool)) .append( Registry.BUCKET_ANIMATION, ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder) .append(GifDrawable.class, new GifDrawableEncoder()) /* GIF Frames */ // Compilation with Gradle requires the type to be specified for UnitModelLoader here. .append( GifDecoder.class, GifDecoder.class, UnitModelLoader.Factory.getInstance()) .append( Registry.BUCKET_BITMAP, GifDecoder.class, Bitmap.class, new GifFrameResourceDecoder(bitmapPool)) /* Drawables */ .append(Uri.class, Drawable.class, resourceDrawableDecoder) .append( Uri.class, Bitmap.class, new ResourceBitmapDecoder(resourceDrawableDecoder, bitmapPool)) /* Files */ .register(new ByteBufferRewinder.Factory()) .append(File.class, ByteBuffer.class, new ByteBufferFileLoader.Factory()) .append(File.class, InputStream.class, new FileLoader.StreamFactory()) .append(File.class, File.class, new FileDecoder()) .append(File.class, ParcelFileDescriptor.class, new FileLoader.FileDescriptorFactory()) // Compilation with Gradle requires the type to be specified for UnitModelLoader here. .append(File.class, File.class, UnitModelLoader.Factory.getInstance()) /* Models */ .register(new InputStreamRewinder.Factory(arrayPool)); if (ParcelFileDescriptorRewinder.isSupported()) { registry.register(new ParcelFileDescriptorRewinder.Factory()); } // DirectResourceLoader and ResourceUriLoader handle resource IDs and Uris owned by this // package. ModelLoaderFactory directResourceLoaderStreamFactory = DirectResourceLoader.inputStreamFactory(context); ModelLoaderFactory directResourceLoaderAssetFileDescriptorFactory = DirectResourceLoader.assetFileDescriptorFactory(context); ModelLoaderFactory directResourceLaoderDrawableFactory = DirectResourceLoader.drawableFactory(context); registry .append(int.class, InputStream.class, directResourceLoaderStreamFactory) .append(Integer.class, InputStream.class, directResourceLoaderStreamFactory) .append( int.class, AssetFileDescriptor.class, directResourceLoaderAssetFileDescriptorFactory) .append( Integer.class, AssetFileDescriptor.class, directResourceLoaderAssetFileDescriptorFactory) .append(int.class, Drawable.class, directResourceLaoderDrawableFactory) .append(Integer.class, Drawable.class, directResourceLaoderDrawableFactory) .append(Uri.class, InputStream.class, ResourceUriLoader.newStreamFactory(context)) .append( Uri.class, AssetFileDescriptor.class, ResourceUriLoader.newAssetFileDescriptorFactory(context)); // ResourceLoader and UriLoader handle resource IDs and Uris owned by other packages. ResourceLoader.UriFactory resourceLoaderUriFactory = new ResourceLoader.UriFactory(resources); ResourceLoader.AssetFileDescriptorFactory resourceLoaderAssetFileDescriptorFactory = new ResourceLoader.AssetFileDescriptorFactory(resources); ResourceLoader.StreamFactory resourceLoaderStreamFactory = new ResourceLoader.StreamFactory(resources); registry .append(Integer.class, Uri.class, resourceLoaderUriFactory) .append(int.class, Uri.class, resourceLoaderUriFactory) .append(Integer.class, AssetFileDescriptor.class, resourceLoaderAssetFileDescriptorFactory) .append(int.class, AssetFileDescriptor.class, resourceLoaderAssetFileDescriptorFactory) .append(Integer.class, InputStream.class, resourceLoaderStreamFactory) .append(int.class, InputStream.class, resourceLoaderStreamFactory); registry .append(String.class, InputStream.class, new DataUrlLoader.StreamFactory()) .append(Uri.class, InputStream.class, new DataUrlLoader.StreamFactory()) .append(String.class, InputStream.class, new StringLoader.StreamFactory()) .append(String.class, ParcelFileDescriptor.class, new StringLoader.FileDescriptorFactory()) .append( String.class, AssetFileDescriptor.class, new StringLoader.AssetFileDescriptorFactory()) .append(Uri.class, InputStream.class, new AssetUriLoader.StreamFactory(context.getAssets())) .append( Uri.class, AssetFileDescriptor.class, new AssetUriLoader.FileDescriptorFactory(context.getAssets())) .append(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context)) .append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { registry.append( Uri.class, InputStream.class, new QMediaStoreUriLoader.InputStreamFactory(context)); registry.append( Uri.class, ParcelFileDescriptor.class, new QMediaStoreUriLoader.FileDescriptorFactory(context)); } boolean useMediaStoreOpenFileApisIfPossible = experiments.isEnabled(UseMediaStoreOpenFileApisIfPossible.class); registry .append( Uri.class, InputStream.class, new UriLoader.StreamFactory(contentResolver, useMediaStoreOpenFileApisIfPossible)) .append( Uri.class, ParcelFileDescriptor.class, new UriLoader.FileDescriptorFactory( contentResolver, useMediaStoreOpenFileApisIfPossible)) .append( Uri.class, AssetFileDescriptor.class, new UriLoader.AssetFileDescriptorFactory( contentResolver, useMediaStoreOpenFileApisIfPossible)) .append(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory()) .append(URL.class, InputStream.class, new UrlLoader.StreamFactory()) .append(Uri.class, File.class, new MediaStoreFileLoader.Factory(context)) .append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory()) .append(byte[].class, ByteBuffer.class, new ByteArrayLoader.ByteBufferFactory()) .append(byte[].class, InputStream.class, new ByteArrayLoader.StreamFactory()) .append(Uri.class, Uri.class, UnitModelLoader.Factory.getInstance()) .append(Drawable.class, Drawable.class, UnitModelLoader.Factory.getInstance()) .append(Drawable.class, Drawable.class, new UnitDrawableDecoder()) /* Transcoders */ .register(Bitmap.class, BitmapDrawable.class, new BitmapDrawableTranscoder(resources)) .register(Bitmap.class, byte[].class, bitmapBytesTranscoder) .register( Drawable.class, byte[].class, new DrawableBytesTranscoder( bitmapPool, bitmapBytesTranscoder, gifDrawableBytesTranscoder)) .register(GifDrawable.class, byte[].class, gifDrawableBytesTranscoder); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ResourceDecoder byteBufferVideoDecoder = VideoDecoder.byteBuffer(bitmapPool); registry.append(ByteBuffer.class, Bitmap.class, byteBufferVideoDecoder); registry.append( ByteBuffer.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, byteBufferVideoDecoder)); } } private static void initializeModules( Context context, Glide glide, Registry registry, List manifestModules, @Nullable AppGlideModule annotationGeneratedModule) { for (GlideModule module : manifestModules) { try { module.registerComponents(context, glide, registry); } catch (AbstractMethodError e) { throw new IllegalStateException( "Attempting to register a Glide v3 module. If you see this, you or one of your" + " dependencies may be including Glide v3 even though you're using Glide v4." + " You'll need to find and remove (or update) the offending dependency." + " The v3 module name is: " + module.getClass().getName(), e); } } if (annotationGeneratedModule != null) { annotationGeneratedModule.registerComponents(context, glide, registry); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/RequestBuilder.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.request.RequestOptions.diskCacheStrategyOf; import static com.bumptech.glide.request.RequestOptions.skipMemoryCacheOf; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources.Theme; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.widget.ImageView; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.ErrorRequestCoordinator; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestCoordinator; import com.bumptech.glide.request.RequestFutureTarget; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.SingleRequest; import com.bumptech.glide.request.ThumbnailRequestCoordinator; import com.bumptech.glide.request.target.PreloadTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.target.ViewTarget; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.signature.AndroidResourceSignature; import com.bumptech.glide.util.Executors; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; import java.io.File; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; /** * A generic class that can handle setting options and staring loads for generic resource types. * * @param The type of resource that will be delivered to the {@link * com.bumptech.glide.request.target.Target}. */ // Public API. @SuppressWarnings({"unused", "WeakerAccess"}) public class RequestBuilder extends BaseRequestOptions> implements Cloneable, ModelTypes> { // Used in generated subclasses protected static final RequestOptions DOWNLOAD_ONLY_OPTIONS = new RequestOptions() .diskCacheStrategy(DiskCacheStrategy.DATA) .priority(Priority.LOW) .skipMemoryCache(true); private final Context context; private final RequestManager requestManager; private final Class transcodeClass; private final Glide glide; private final GlideContext glideContext; @NonNull @SuppressWarnings("unchecked") private TransitionOptions transitionOptions; @Nullable private Object model; // model may occasionally be null, so to enforce that load() was called, put a boolean rather // than relying on model not to be null. @Nullable private List> requestListeners; @Nullable private RequestBuilder thumbnailBuilder; @Nullable private RequestBuilder errorBuilder; @Nullable private Float thumbSizeMultiplier; private boolean isDefaultTransitionOptionsSet = true; private boolean isModelSet; private boolean isThumbnailBuilt; // We only override the method to change the return type, not the functionality. @SuppressLint("CheckResult") @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") protected RequestBuilder( @NonNull Glide glide, RequestManager requestManager, Class transcodeClass, Context context) { this.glide = glide; this.requestManager = requestManager; this.transcodeClass = transcodeClass; this.context = context; this.transitionOptions = requestManager.getDefaultTransitionOptions(transcodeClass); this.glideContext = glide.getGlideContext(); initRequestListeners(requestManager.getDefaultRequestListeners()); apply(requestManager.getDefaultRequestOptions()); } RequestManager getRequestManager() { return requestManager; } @SuppressLint("CheckResult") @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") protected RequestBuilder(Class transcodeClass, RequestBuilder other) { this(other.glide, other.requestManager, transcodeClass, other.context); model = other.model; isModelSet = other.isModelSet; // This is safe because it will always mutate, no one else has access to the object. apply(other); } // Casting from Object to a specific type is always safe. @SuppressWarnings("unchecked") // addListener always returns the same instance. @SuppressLint("CheckResult") private void initRequestListeners(List> requestListeners) { for (RequestListener listener : requestListeners) { addListener((RequestListener) listener); } } /** * Applies the given options to the request. * *

    As with {@link RequestOptions#apply(BaseRequestOptions)}, {@code #apply} only replaces those * values that are explicitly set in the given {@link RequestOptions} object. If you need to * completely reset all previously set options, create a new {@code RequestBuilder} instead of * using this method. * * @see RequestOptions#apply(BaseRequestOptions) * @return This request builder. */ @NonNull @CheckResult @Override public RequestBuilder apply(@NonNull BaseRequestOptions requestOptions) { Preconditions.checkNotNull(requestOptions); return super.apply(requestOptions); } /** * Sets the {@link TransitionOptions} to use to transition from the placeholder or thumbnail when * this load completes. * *

    The given {@link TransitionOptions} will replace any {@link TransitionOptions} set * previously. * * @return This request builder. */ @NonNull @CheckResult public RequestBuilder transition( @NonNull TransitionOptions transitionOptions) { if (isAutoCloneEnabled()) { return clone().transition(transitionOptions); } this.transitionOptions = Preconditions.checkNotNull(transitionOptions); isDefaultTransitionOptionsSet = false; return selfOrThrowIfLocked(); } /** * Sets a {@link RequestListener} to monitor the resource load and removes all previously set * listeners (either via this method or from {@link #addListener(RequestListener)} . * *

    Calls to this method will replace previously set listeners. To set multiple listeners, use * {@link #addListener} instead. * * @param requestListener The request listener to use. * @return This request builder. */ @NonNull @CheckResult @SuppressWarnings("unchecked") public RequestBuilder listener( @Nullable RequestListener requestListener) { if (isAutoCloneEnabled()) { return clone().listener(requestListener); } this.requestListeners = null; return addListener(requestListener); } /** * Adds a {@link RequestListener} to the list that will be called in the order they were added * when the request ends. * *

    Multiple calls to this method append additional listeners. Previous listeners are not * removed. If you want to replace any previously added listeners, use {@link * #listener(RequestListener)}. * *

    Listeners track the state of the request started by this particular {@code builder}. When * used with the thumbnail APIs ({@link #thumbnail(RequestBuilder)}) this can start to seem * confusing because multiple requests are running and each may succeed or fail, independent of * each other. As a rule, Glide does not add {@link RequestListener}s to thumbnail requests * automatically. That means that {@link RequestListener}s track the state of exactly one request * in the chain. For example, if you start a primary request with a single nested thumbnail and * you add a {@link RequestListener} only to the primary request, then the {@link RequestListener} * will only be notified when the primary request succeeds or fails. If the thumbnail succeeds, * but the primary request fails, the {@link RequestListener} added to the primary request will * still be called with {@link RequestListener#onLoadFailed(GlideException, Object, Target, * boolean)}. In the same scenario, the {@link RequestListener} added only to the primary request * will not have {@link RequestListener#onResourceReady(Object, Object, Target, DataSource, * boolean)} called when the thumbnail request finishes successfully. Similarly, if you add a * {@link RequestListener} only to a thumbnail request, but not the primary request, that {@code * listener} will only be called for changes related to the thumbnail request. If the thumbnail * request fails, the {@code listener} added to the thumbnail request will be immediately called * via {@link RequestListener#onLoadFailed(GlideException, Object, Target, boolean)}, even though * the primary request may eventually succeed. It is perfectly possible to add a {@link * RequestListener} to both the primary and a thumbnail request. If you do so, the {@link * RequestListener} will be called independently for each request when it finishes. Keep in mind * that if any parent request finishes before its thumbnail request(s), it will attempt to cancel * those requests. As a result there's no guarantee that a {@link RequestListener} added to a * thumbnail request will actually be called with either success or failure. These same patterns * hold for arbitrarily nested thumbnails. The {@code listener} is only called for the requests it * is added to and may not be called for every thumbnail request if those requests are cancelled * due to the completion of a parent request. * *

    The one exception to the rules about thumbnails is {@link #thumbnail(float)}. In this case * we appear to be passing {@link RequestListener}s added to the parent request to the generated * thumbnail requests. To try to reduce confusion, the {@link #thumbnail(float)} method has been * deprecated. It can be easily replicated using {@link #thumbnail(RequestBuilder)} and {@link * BaseRequestOptions#sizeMultiplier(float)}. * *

    Often in UIs it's desirable to try to track the overall status of a request, including the * thumbnails. For example, you might want to load an image, start an animation if the * asynchronous image load succeeds and perform some fallback action if it fails. If you're using * a single primary request, {@link RequestListener} will work for this. However, if you then * decide to try to make things more performant by adding a thumbnail (or multiple thumbnails), * {@link RequestListener} is awkward because either you only add it to the main request and it's * not called when the thumbnails complete (which defeats the purpose) or it's called for every * request and it's hard to keep track of when the overall request has failed. A better option * than using {@link RequestListener} to track the state of the UI then is to use {@link Target} * instead. {@link Target#onResourceReady(Object, Transition)} will be called when any thumbnail * finishes, which you can use to trigger your animation starting. {@link * Target#onLoadFailed(Drawable)} will only be called if every request in the chain, including the * primary request, fails, which you can use to trigger your fallback behavior. Be sure to pick an * appropriate {@link Target} subclass when possible, like {@link * com.bumptech.glide.request.target.BitmapImageViewTarget} or {@link * com.bumptech.glide.request.target.DrawableImageViewTarget} when loading into {@link ImageView} * or {@link com.bumptech.glide.request.target.CustomTarget} when using custom rendering. Don't * forget to call {@code super()} in the {@code ImageViewTarget}s. * *

    It's best to create a single instance of an exception handler per type of request (usually * activity/fragment) rather than pass one in per request to avoid some redundant object * allocation. * * @param requestListener The request listener to use. If {@code null}, this method is a noop. * @return This request builder. */ @NonNull @CheckResult public RequestBuilder addListener( @Nullable RequestListener requestListener) { if (isAutoCloneEnabled()) { return clone().addListener(requestListener); } if (requestListener != null) { if (this.requestListeners == null) { this.requestListeners = new ArrayList<>(); } this.requestListeners.add(requestListener); } return selfOrThrowIfLocked(); } /** * Sets a {@link RequestBuilder} that is built and run if the load started by this {@link * RequestBuilder} fails. * *

    If this {@link RequestBuilder} uses a thumbnail that succeeds the given error {@link * RequestBuilder} will be started anyway if the non-thumbnail request fails. * *

    Recursive calls to this method as well as calls to {@link #thumbnail(float)} and {@link * #thumbnail(RequestBuilder)} are supported for the given error {@link RequestBuilder}. * *

    Unlike {@link #thumbnail(RequestBuilder)} and {@link #thumbnail(float)}, no options from * this primary {@link RequestBuilder} are propagated to the given error {@link RequestBuilder}. * Options like priority, override widths and heights and transitions must be applied * independently to the error builder. * *

    The given {@link RequestBuilder} will start and potentially override a fallback drawable if * it's set on this {@link RequestBuilder} via {@link * RequestOptions#fallback(android.graphics.drawable.Drawable)} or {@link * RequestOptions#fallback(int)}. * * @return This {@link RequestBuilder}. */ @NonNull public RequestBuilder error(@Nullable RequestBuilder errorBuilder) { if (isAutoCloneEnabled()) { return clone().error(errorBuilder); } this.errorBuilder = errorBuilder; return selfOrThrowIfLocked(); } /** * Identical to calling {@link #error(RequestBuilder)} where the {@code RequestBuilder} is the * result of calling {@link #clone()} and removing any existing thumbnail and error {@code * RequestBuilders}. * *

    Other than thumbnail and error {@code RequestBuilder}s, which are removed, all other options * are retained from the primary request. However, order matters! Any options applied after * this method is called will not be applied to the error {@code RequestBuilder}. * *

    WARNING: Calling this method with a {@code model} whose type does not match the type of the * model passed to {@code load()} may be dangerous! Any options that were applied by the various * type specific {@code load()} methods, like {@link #load(byte[])} will be copied to the error * request here even if the {@code model} you pass to this method doesn't match. Similary, options * that would be normally applied by type specific {@code load()} methods will not be * applied to this request. If this behavior is confusing or unexpected, use {@link * #error(RequestBuilder)} instead. */ @NonNull @CheckResult public RequestBuilder error(Object model) { if (model == null) { return error((RequestBuilder) null); } return error(cloneWithNullErrorAndThumbnail().load(model)); } private RequestBuilder cloneWithNullErrorAndThumbnail() { return clone() .error((RequestBuilder) null) .thumbnail((RequestBuilder) null); } /** * Loads and displays the resource retrieved by the given thumbnail request if it finishes before * this request. Best used for loading thumbnail resources that are smaller and will be loaded * more quickly than the full size resource. There are no guarantees about the order in which the * requests will actually finish. However, if the thumb request completes after the full request, * the thumb resource will never replace the full resource. * *

    Recursive calls to thumbnail are supported. * *

    Overrides any previous calls to this method, {@link #thumbnail(float)} and {@link * #thumbnail(RequestBuilder[])}. * * @see #thumbnail(float) * @see #thumbnail(RequestBuilder[]) * @param thumbnailRequest The request to use to load the thumbnail. * @return This request builder. */ @NonNull @CheckResult @SuppressWarnings("unchecked") public RequestBuilder thumbnail( @Nullable RequestBuilder thumbnailRequest) { if (isAutoCloneEnabled()) { return clone().thumbnail(thumbnailRequest); } this.thumbnailBuilder = thumbnailRequest; return selfOrThrowIfLocked(); } /** * Recursively applies {@link #thumbnail(RequestBuilder)} so that the {@link RequestBuilder}s are * loaded as thumbnails in the given priority order. * *

    {@link #thumbnail(RequestBuilder)} is applied in the order given so that the {@link * RequestBuilder} at position 0 has the {@link RequestBuilder} at position 1 applied as using its * thumbnail method, the {@link RequestBuilder} at position 1 has the {@link RequestBuilder} at * position 2 applied using its thumbnail method and so on. * *

    Calling this method with an {@code null} array of {@link RequestBuilder} thumbnails or an * empty array of {@link RequestBuilder} thumbnails is equivalent to calling {@link * #thumbnail(RequestBuilder)} with {@code null}. * *

    Any individual {@link RequestBuilder} in the array of thumbnails provided here may be {@code * null}. {@code null} {@link RequestBuilder}s are ignored and excluded from the recursive chain. * *

    The {@link RequestBuilder} objects provided here may be mutated and have any previous calls * to this method or {@link #thumbnail(RequestBuilder)} methods overridden. * *

    Overrides any previous calls to {@link #thumbnail(RequestBuilder)}, {@link * #thumbnail(float)} and this method. * * @see #thumbnail(float) * @see #thumbnail(RequestBuilder) * @return This request builder. */ @SuppressWarnings({"CheckResult", "unchecked"}) @NonNull @CheckResult public RequestBuilder thumbnail( @Nullable RequestBuilder... thumbnails) { if (thumbnails == null || thumbnails.length == 0) { return thumbnail((RequestBuilder) null); } return thumbnail(Arrays.asList(thumbnails)); } /** * Recursively applies {@link #thumbnail(RequestBuilder)} so that the {@link RequestBuilder}s are * loaded as thumbnails in the given priority order. * *

    {@link #thumbnail(RequestBuilder)} is applied in the order given so that the {@link * RequestBuilder} at position 0 has the {@link RequestBuilder} at position 1 applied as using its * thumbnail method, the {@link RequestBuilder} at position 1 has the {@link RequestBuilder} at * position 2 applied using its thumbnail method and so on. * *

    Calling this method with a {@code null} list of {@link RequestBuilder} thumbnails or an * empty list of {@link RequestBuilder} thumbnails is equivalent to calling {@link * #thumbnail(RequestBuilder)} with {@code null}. * *

    Any individual {@link RequestBuilder} in the list of thumbnails provided here may be {@code * null}. {@code null} {@link RequestBuilder}s are ignored and excluded from the recursive chain. * *

    The {@link RequestBuilder} objects provided here may be mutated and have any previous calls * to this method or {@link #thumbnail(RequestBuilder)} methods overridden. * *

    Overrides any previous calls to {@link #thumbnail(RequestBuilder)}, {@link * #thumbnail(float)} and this method. * * @see #thumbnail(float) * @see #thumbnail(RequestBuilder) * @return This request builder. */ @SuppressWarnings({"CheckResult", "unchecked"}) @NonNull @CheckResult public RequestBuilder thumbnail( @Nullable List> thumbnails) { if (thumbnails == null || thumbnails.isEmpty()) { return thumbnail((RequestBuilder) null); } RequestBuilder previous = null; // Start with the lowest priority thumbnail so that we can safely handle mutations if // autoClone() is enabled by assigning the result of calling thumbnail() during the iteration. // Starting with the highest priority thumbnail would prevent us from assigning the result of // thumbnail because the mutated request wouldn't be used in the next iteration. for (int i = thumbnails.size() - 1; i >= 0; i--) { RequestBuilder current = thumbnails.get(i); // Ignore null thumbnails. if (current == null) { continue; } if (previous == null) { // If we don't yet have our first non-null request, set it and continue. previous = current; } else { // Otherwise make our next lowest priority request the thumbnail of our current request. previous = current.thumbnail(previous); } } return thumbnail(previous); } /** * Loads a resource in an identical manner to this request except with the dimensions of the * target multiplied by the given size multiplier. If the thumbnail load completes before the full * size load, the thumbnail will be shown. If the thumbnail load completes after the full size * load, the thumbnail will not be shown. * *

    Note - The thumbnail resource will be smaller than the size requested so the target (or * {@link ImageView}) must be able to scale the thumbnail appropriately. See {@link * android.widget.ImageView.ScaleType}. * *

    Almost all options will be copied from the original load, including the {@link * com.bumptech.glide.load.model.ModelLoader}, {@link com.bumptech.glide.load.ResourceDecoder}, * and {@link com.bumptech.glide.load.Transformation}s. However, {@link * com.bumptech.glide.request.RequestOptions#placeholder(int)} and {@link * com.bumptech.glide.request.RequestOptions#error(int)}, and {@link #listener(RequestListener)} * will only be used on the full size load and will not be copied for the thumbnail load. * *

    Recursive calls to thumbnail are supported. * *

    Overrides any previous calls to this method, {@link #thumbnail(RequestBuilder[])}, and * {@link #thumbnail(RequestBuilder)}. * * @see #thumbnail(RequestBuilder) * @see #thumbnail(RequestBuilder[]) * @param sizeMultiplier The multiplier to apply to the {@link Target}'s dimensions when loading * the thumbnail. * @return This request builder. * @deprecated The behavior differences between this method and {@link #thumbnail(RequestBuilder)} * are subtle, hard to understand for users and hard to maintain for developers. See the * javadoc on {@link #listener(RequestListener)} for one concrete example of the behavior * differences and complexity introduced by this method. Better consistency and readability * can be obtained by calling {@link #thumbnail(RequestBuilder)} with a duplicate {@code * RequestBuilder} on which you have called {@link BaseRequestOptions#sizeMultiplier(float)}. * In practice this method also isn't especially useful. It's much more common to want to * specify a number of different attributes for thumbnails than just a simple percentage * modifier on the target size, so there's little justification for keeping this method. This * method will be removed in a future version of Glide. */ @NonNull @CheckResult @SuppressWarnings("unchecked") @Deprecated public RequestBuilder thumbnail(float sizeMultiplier) { if (isAutoCloneEnabled()) { return clone().thumbnail(sizeMultiplier); } if (sizeMultiplier < 0f || sizeMultiplier > 1f) { throw new IllegalArgumentException("sizeMultiplier must be between 0 and 1"); } this.thumbSizeMultiplier = sizeMultiplier; return selfOrThrowIfLocked(); } /** * Sets the specific model to load data for. * * @param model The model to load data for, or null. * @return This request builder. */ @NonNull @CheckResult @SuppressWarnings("unchecked") @Override public RequestBuilder load(@Nullable Object model) { return loadGeneric(model); } @NonNull private RequestBuilder loadGeneric(@Nullable Object model) { if (isAutoCloneEnabled()) { return clone().loadGeneric(model); } this.model = model; isModelSet = true; return selfOrThrowIfLocked(); } /** * Returns an object to load the given {@link Bitmap}. * *

    It's almost always better to allow Glide to load {@link Bitmap}s than pass {@link Bitmap}s * into Glide. If you have a custom way to obtain {@link Bitmap}s that is not supported by Glide * by default, consider registering a custom {@link com.bumptech.glide.load.model.ModelLoader} or * {@link com.bumptech.glide.load.ResourceDecoder} instead of using this method. * *

    The {@link DiskCacheStrategy} is set to {@link DiskCacheStrategy#NONE}. Previous calls to * {@link #apply(BaseRequestOptions)} or previously applied {@link DiskCacheStrategy}s will be * overridden by this method. Applying an {@link DiskCacheStrategy} other than {@link * DiskCacheStrategy#NONE} after calling this method may result in undefined behavior. * *

    In memory caching relies on Object equality. The contents of the {@link Bitmap}s are not * compared. * * @see #load(Object) */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable Bitmap bitmap) { return loadGeneric(bitmap).apply(diskCacheStrategyOf(DiskCacheStrategy.NONE)); } /** * Returns a request builder to load the given {@link Drawable}. * *

    It's almost always better to allow Glide to load {@link Bitmap}s than to pass {@link * Bitmap}s into Glide using this method . If you have a custom way to obtain {@link Bitmap}s that * is not supported by Glide by default, consider registering a custom {@link * com.bumptech.glide.load.model.ModelLoader} or {@link com.bumptech.glide.load.ResourceDecoder} * instead of using this method. * *

    The {@link DiskCacheStrategy} is set to {@link DiskCacheStrategy#NONE}. Previous calls to * {@link #apply(BaseRequestOptions)} or previously applied {@link DiskCacheStrategy}s will be * overridden by this method. Applying an {@link DiskCacheStrategy} other than {@link * DiskCacheStrategy#NONE} after calling this method may result in undefined behavior. * *

    In memory caching relies on Object equality. The contents of the {@link Drawable}s are not * compared. * * @see #load(Object) */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable Drawable drawable) { return loadGeneric(drawable).apply(diskCacheStrategyOf(DiskCacheStrategy.NONE)); } /** * Returns a request builder to load the given {@link java.lang.String}. * *

    Note - this method caches data using only the given String as the cache key. If the data is * a Uri outside of your control, or you otherwise expect the data represented by the given String * to change without the String identifier changing, Consider using {@link * com.bumptech.glide.request.RequestOptions#signature(com.bumptech.glide.load.Key)} to mixin a * signature you create that identifies the data currently at the given String that will * invalidate the cache if that data changes. Alternatively, using {@link * com.bumptech.glide.load.engine.DiskCacheStrategy#NONE} and/or {@link * com.bumptech.glide.request.RequestOptions#skipMemoryCache(boolean)} may be appropriate. * *

    If {@code string} is in fact a resource {@link Uri}, you should first parse it to a Uri * using {@link Uri#parse(String)} and then pass the {@code Uri} to {@link #load(Uri)}. Doing so * will ensure that we respect the appropriate theme / dark / light mode. As an alternative, you * can also manually apply the current {@link Theme} using {@link #theme(Theme)}. * * @see #load(Object) * @param string A file path, or a uri or url handled by {@link * com.bumptech.glide.load.model.UriLoader}. */ @NonNull @Override @CheckResult public RequestBuilder load(@Nullable String string) { return loadGeneric(string); } /** * Returns a request builder to load the given {@link Uri}. * *

    Note - this method caches data at Uris using only the Uri itself as the cache key. The data * represented by Uris from some content providers may change without the Uri changing, which * means using this method can lead to displaying stale data. Consider using {@link * com.bumptech.glide.request.RequestOptions#signature(com.bumptech.glide.load.Key)} to mixin a * signature you create based on the data at the given Uri that will invalidate the cache if that * data changes. Alternatively, using {@link * com.bumptech.glide.load.engine.DiskCacheStrategy#NONE} and/or {@link * com.bumptech.glide.request.RequestOptions#skipMemoryCache(boolean)} may be appropriate. The * only exception to this is that if we recognize the given {@code uri} as having {@link * ContentResolver#SCHEME_ANDROID_RESOURCE}, then we'll apply {@link AndroidResourceSignature} * automatically. If we do so, calls to other {@code load()} methods will not override * the automatically applied signature. * *

    If {@code uri} has a {@link Uri#getScheme()} of {@link * android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}, then this method will add the {@link * android.content.res.Resources.Theme} of the {@link Context} associated with this {@code * requestBuilder} so that we can respect themeable attributes and/or light / dark mode. Any call * to {@link #theme(Theme)} prior to this method call will be overridden. To avoid this, call * {@link #theme(Theme)} after calling this method with either {@code null} or the {@code Theme} * you'd prefer to use instead. Note that even if you change the theme, the {@link * AndroidResourceSignature} will still be based on the {@link Context} theme. * * @see #load(Object) * @param uri The Uri representing the image. Must be of a type handled by {@link * com.bumptech.glide.load.model.UriLoader}. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable Uri uri) { return maybeApplyOptionsResourceUri(uri, loadGeneric(uri)); } private RequestBuilder maybeApplyOptionsResourceUri( @Nullable Uri uri, RequestBuilder requestBuilder) { if (uri == null || !ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) { return requestBuilder; } return applyResourceThemeAndSignature(requestBuilder); } private RequestBuilder applyResourceThemeAndSignature( RequestBuilder requestBuilder) { return requestBuilder .theme(context.getTheme()) .signature(AndroidResourceSignature.obtain(context)); } /** * Returns a request builder to load the given {@link File}. * *

    Note - this method caches data for Files using only the file path itself as the cache key. * The data in the File can change so using this method can lead to displaying stale data. If you * expect the data in the File to change, Consider using {@link * com.bumptech.glide.request.RequestOptions#signature(com.bumptech.glide.load.Key)} to mixin a * signature you create that identifies the data currently in the File that will invalidate the * cache if that data changes. Alternatively, using {@link * com.bumptech.glide.load.engine.DiskCacheStrategy#NONE} and/or {@link * com.bumptech.glide.request.RequestOptions#skipMemoryCache(boolean)} may be appropriate. * * @see #load(Object) * @param file The File containing the image */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable File file) { return loadGeneric(file); } /** * Returns a request builder that uses the {@link * com.bumptech.glide.load.model.ModelLoaderFactory} currently registered or {@link Integer} to * load the image represented by the given {@link Integer} resource id. Defaults to {@link * com.bumptech.glide.load.model.ResourceLoader} to load resource id models. * *

    By default this method adds a version code and night mode based signature to the cache key * used to cache this resource in Glide. This signature is sufficient to guarantee that end users * will see the most up to date versions of your Drawables, but during development if you do not * increment your version code before each install and you replace a Drawable with different data * without changing the Drawable name, you may see inconsistent cached data. To get around this, * consider using {@link com.bumptech.glide.load.engine.DiskCacheStrategy#NONE} via {@link * RequestOptions#diskCacheStrategy(com.bumptech.glide.load.engine.DiskCacheStrategy)} during * development, and re-enabling the default {@link * com.bumptech.glide.load.engine.DiskCacheStrategy#RESOURCE} for release builds. * *

    This method will load non-{@link android.graphics.Bitmap} resources like {@link * android.graphics.drawable.VectorDrawable}s. Although Glide makes a best effort to apply {@link * com.bumptech.glide.load.Transformation}s to these {@link Drawable}s by either extracting the * underlying {@link Bitmap} or by converting the {@link Drawable} to a {@link Bitmap}, Glide is * still not able to transform all types of resources. Animated {@link Drawable}s cannot be * transformed (other than {@link com.bumptech.glide.load.resource.gif.GifDrawable}). To avoid * load failures if a {@link Drawable} can't be transformed, use the optional transformation * methods like {@link RequestOptions#optionalTransform(Class, Transformation)}. * *

    In some cases converting {@link Drawable}s to {@link Bitmap}s may be inefficient. Use this * method, especially in conjunction with {@link com.bumptech.glide.load.Transformation}s with * caution for non-{@link Bitmap} {@link Drawable}s. * *

    This method will add the {@link android.content.res.Resources.Theme} of the {@link Context} * associated with this {@code requestBuilder} so that we can respect themeable attributes and/or * light / dark mode. Any call to {@link #theme(Theme)} prior to this method call will be * overridden. To avoid this, call {@link #theme(Theme)} after calling this method with either * {@code null} or the {@code Theme} you'd prefer to use instead. Note that even if you change the * theme, the {@link AndroidResourceSignature} will still be based on the {@link Context} theme. * * @see #load(Integer) * @see com.bumptech.glide.signature.AndroidResourceSignature */ @NonNull @CheckResult @Override public RequestBuilder load(@RawRes @DrawableRes @Nullable Integer resourceId) { return applyResourceThemeAndSignature(loadGeneric(resourceId)); } /** * Returns a request builder to load the given {@link URL}. * * @param url The URL representing the image. * @see #load(Object) * @deprecated The {@link java.net.URL} class has a number of * performance problems and should generally be avoided when possible. Prefer {@link * #load(android.net.Uri)} or {@link #load(String)}. */ @Deprecated @CheckResult @Override public RequestBuilder load(@Nullable URL url) { return loadGeneric(url); } /** * Returns a request to load the given byte array. * *

    Note - by default loads for bytes are not cached in either the memory or the disk cache. * * @param model the data to load. * @see #load(Object) */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable byte[] model) { RequestBuilder result = loadGeneric(model); if (!result.isDiskCacheStrategySet()) { result = result.apply(diskCacheStrategyOf(DiskCacheStrategy.NONE)); } if (!result.isSkipMemoryCacheSet()) { result = result.apply(skipMemoryCacheOf(true /*skipMemoryCache*/)); } return result; } /** * Returns a copy of this request builder with all of the options put so far on this builder. * *

    This method returns a "deep" copy in that all non-immutable arguments are copied such that * changes to one builder will not affect the other builder. However, in addition to immutable * arguments, the current model is not copied so changes to the model will affect both builders. */ @SuppressWarnings({ // we don't want to throw to be user friendly "PMD.CloneThrowsCloneNotSupportedException" }) @CheckResult @Override public RequestBuilder clone() { RequestBuilder result = super.clone(); result.transitionOptions = result.transitionOptions.clone(); if (result.requestListeners != null) { result.requestListeners = new ArrayList<>(result.requestListeners); } if (result.thumbnailBuilder != null) { result.thumbnailBuilder = result.thumbnailBuilder.clone(); } if (result.errorBuilder != null) { result.errorBuilder = result.errorBuilder.clone(); } return result; } /** * Set the target the resource will be loaded into. * * @param target The target to load the resource into. * @return The given target. * @see RequestManager#clear(Target) */ @NonNull public > Y into(@NonNull Y target) { return into(target, /* targetListener= */ null, Executors.mainThreadExecutor()); } /** * Set the target the resource will be loaded into; the callback will be set at the front of the * queue. * * @param target The target to load the resource into. * @return The given target. * @see RequestManager#clear(Target) */ @NonNull public > Y experimentalIntoFront(@NonNull Y target) { return into(target, /* targetListener= */ null, Executors.mainThreadExecutorFront()); } @NonNull public > Y into( @NonNull Y target, @Nullable RequestListener targetListener, Executor callbackExecutor) { return into(target, targetListener, /* options= */ this, callbackExecutor); } private > Y into( @NonNull Y target, @Nullable RequestListener targetListener, BaseRequestOptions options, Executor callbackExecutor) { Preconditions.checkNotNull(target); if (!isModelSet) { throw new IllegalArgumentException("You must call #load() before calling #into()"); } Request request = buildRequest(target, targetListener, options, callbackExecutor); Request previous = target.getRequest(); if (request.isEquivalentTo(previous) && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) { // If the request is completed, beginning again will ensure the result is re-delivered, // triggering RequestListeners and Targets. If the request is failed, beginning again will // restart the request, giving it another chance to complete. If the request is already // running, we can let it continue running without interruption. if (!Preconditions.checkNotNull(previous).isRunning()) { // Use the previous request rather than the new one to allow for optimizations like skipping // setting placeholders, tracking and un-tracking Targets, and obtaining View dimensions // that are done in the individual Request. previous.begin(); } return target; } requestManager.clear(target); target.setRequest(request); requestManager.track(target, request); return target; } // If the caller is using skipMemoryCache and the previous request is finished, calling begin on // the previous request will complete from memory because it will just use the resource that had // already been loaded. If the previous request isn't complete, we can wait for it to finish // because the previous request must also be using skipMemoryCache for the requests to be // equivalent. See #2663 for additional context. private boolean isSkipMemoryCacheWithCompletePreviousRequest( BaseRequestOptions options, Request previous) { return !options.isMemoryCacheable() && previous.isComplete(); } /** * Sets the {@link ImageView} the resource will be loaded into, cancels any existing loads into * the view, and frees any resources Glide may have previously loaded into the view so they may be * reused. * * @see RequestManager#clear(Target) * @param view The view to cancel previous loads for and load the new resource into. * @return The {@link com.bumptech.glide.request.target.Target} used to wrap the given {@link * ImageView}. */ @NonNull public ViewTarget into(@NonNull ImageView view) { Util.assertMainThread(); Preconditions.checkNotNull(view); BaseRequestOptions requestOptions = this; if (!requestOptions.isTransformationSet() && requestOptions.isTransformationAllowed() && view.getScaleType() != null) { // Clone in this method so that if we use this RequestBuilder to load into a View and then // into a different target, we don't retain the transformation applied based on the previous // View's scale type. switch (view.getScaleType()) { case CENTER_CROP: requestOptions = requestOptions.clone().optionalCenterCrop(); break; case CENTER_INSIDE: requestOptions = requestOptions.clone().optionalCenterInside(); break; case FIT_CENTER: case FIT_START: case FIT_END: requestOptions = requestOptions.clone().optionalFitCenter(); break; case FIT_XY: requestOptions = requestOptions.clone().optionalCenterInside(); break; case CENTER: case MATRIX: default: // Do nothing. } } return into( glideContext.buildImageViewTarget(view, transcodeClass), /* targetListener= */ null, requestOptions, Executors.mainThreadExecutor()); } /** * Sets the {@link ImageView} the resource will be loaded into, cancels any existing loads into * the view, and frees any resources Glide may have previously loaded into the view so they may be * reused; the callback will be set at the front of the queue. * * @see RequestManager#clear(Target) * @param view The view to cancel previous loads for and load the new resource into. * @return The {@link com.bumptech.glide.request.target.Target} used to wrap the given {@link * ImageView}. */ @NonNull public ViewTarget experimentalIntoFront(@NonNull ImageView view) { Util.assertMainThread(); Preconditions.checkNotNull(view); BaseRequestOptions requestOptions = this; if (!requestOptions.isTransformationSet() && requestOptions.isTransformationAllowed() && view.getScaleType() != null) { // Clone in this method so that if we use this RequestBuilder to load into a View and then // into a different target, we don't retain the transformation applied based on the previous // View's scale type. switch (view.getScaleType()) { case CENTER_CROP: requestOptions = requestOptions.clone().optionalCenterCrop(); break; case CENTER_INSIDE: requestOptions = requestOptions.clone().optionalCenterInside(); break; case FIT_CENTER: case FIT_START: case FIT_END: requestOptions = requestOptions.clone().optionalFitCenter(); break; case FIT_XY: requestOptions = requestOptions.clone().optionalCenterInside(); break; case CENTER: case MATRIX: default: // Do nothing. } } return into( glideContext.buildImageViewTarget(view, transcodeClass), /* targetListener= */ null, requestOptions, Executors.mainThreadExecutorFront()); } /** * Returns a future that can be used to do a blocking get on a background thread. * * @param width The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)} if * previously called. * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)}} if * previously called). * @see RequestManager#clear(Target) * @deprecated Use {@link #submit(int, int)} instead. */ @Deprecated public FutureTarget into(int width, int height) { return submit(width, height); } /** * Returns a future that can be used to do a blocking get on a background thread. * *

    This method defaults to {@link Target#SIZE_ORIGINAL} for the width and the height. However, * since the width and height will be overridden by values passed to {@link * RequestOptions#override(int, int)}, this method can be used whenever {@link RequestOptions} * with override values are applied, or whenever you want to retrieve the image in its original * size. * * @see #submit(int, int) * @see #into(Target) */ @NonNull public FutureTarget submit() { return submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); } /** * Returns a future that can be used to do a blocking get on a background thread. * * @param width The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)} if * previously called. * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)}} if * previously called). */ @NonNull public FutureTarget submit(int width, int height) { final RequestFutureTarget target = new RequestFutureTarget<>(width, height); return into(target, target, Executors.directExecutor()); } /** * Preloads the resource into the cache using the given width and height. * *

    Pre-loading is useful for making sure that resources you are going to to want in the near * future are available quickly. * *

    Note - Any thumbnail request that does not complete before the primary request will be * cancelled and may not be preloaded successfully. Cancellation of outstanding thumbnails after * the primary request succeeds is a common behavior of all Glide requests. We do not try to * prevent that behavior here. If you absolutely need all thumbnails to be preloaded individually, * make separate preload() requests for each thumbnail (you can still combine them into one call * when loading the image(s) into the UI in a subsequent request). * * @param width The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)} if * previously called. * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)}} if * previously called). * @return A {@link Target} that can be used to cancel the load via {@link * RequestManager#clear(Target)}. * @see com.bumptech.glide.ListPreloader */ @NonNull public Target preload(int width, int height) { final PreloadTarget target = PreloadTarget.obtain(requestManager, width, height); return into(target); } /** * Preloads the resource into the cache using the given width and height; the callback will be set * at the front of the queue. * *

    Pre-loading is useful for making sure that resources you are going to to want in the near * future are available quickly. * *

    Note - Any thumbnail request that does not complete before the primary request will be * cancelled and may not be preloaded successfully. Cancellation of outstanding thumbnails after * the primary request succeeds is a common behavior of all Glide requests. We do not try to * prevent that behavior here. If you absolutely need all thumbnails to be preloaded individually, * make separate preload() requests for each thumbnail (you can still combine them into one call * when loading the image(s) into the UI in a subsequent request). * * @param width The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)} if * previously called. * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be * overridden by {@link com.bumptech.glide.request.RequestOptions#override(int, int)}} if * previously called). * @return A {@link Target} that can be used to cancel the load via {@link * RequestManager#clear(Target)}. * @see com.bumptech.glide.ListPreloader */ @NonNull public Target experimentalPreloadFront(int width, int height) { final PreloadTarget target = PreloadTarget.obtain(requestManager, width, height); return experimentalIntoFront(target); } /** * Preloads the resource into the cache using {@link Target#SIZE_ORIGINAL} as the target width and * height. Equivalent to calling {@link #preload(int, int)} with {@link Target#SIZE_ORIGINAL} as * the width and height. * * @return A {@link Target} that can be used to cancel the load via {@link * RequestManager#clear(Target)} * @see #preload(int, int) */ @NonNull public Target preload() { return preload(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); } /** * Loads the original unmodified data into the cache and calls the given Target with the cache * File. * * @param target The Target that will receive the cache File when the load completes * @param The type of Target. * @return The given Target. * @deprecated Use {@link RequestManager#downloadOnly()} and {@link #into(Target)}. */ @Deprecated @CheckResult public > Y downloadOnly(@NonNull Y target) { return getDownloadOnlyRequest().into(target); } /** * Loads the original unmodified data into the cache and returns a {@link * java.util.concurrent.Future} that can be used to retrieve the cache File containing the data. * * @param width The width in pixels to use to fetch the data. * @param height The height in pixels to use to fetch the data. * @return A {@link java.util.concurrent.Future} that can be used to retrieve the cache File * containing the data. * @deprecated Use {@link RequestManager#downloadOnly()} and {@link #submit(int, int)}. */ @Deprecated @CheckResult public FutureTarget downloadOnly(int width, int height) { return getDownloadOnlyRequest().submit(width, height); } @NonNull @CheckResult protected RequestBuilder getDownloadOnlyRequest() { return new RequestBuilder<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS); } @NonNull private Priority getThumbnailPriority(@NonNull Priority current) { switch (current) { case LOW: return Priority.NORMAL; case NORMAL: return Priority.HIGH; case HIGH: case IMMEDIATE: return Priority.IMMEDIATE; default: throw new IllegalArgumentException("unknown priority: " + getPriority()); } } private Request buildRequest( Target target, @Nullable RequestListener targetListener, BaseRequestOptions requestOptions, Executor callbackExecutor) { return buildRequestRecursive( /* requestLock= */ new Object(), target, targetListener, /* parentCoordinator= */ null, transitionOptions, requestOptions.getPriority(), requestOptions.getOverrideWidth(), requestOptions.getOverrideHeight(), requestOptions, callbackExecutor); } private Request buildRequestRecursive( Object requestLock, Target target, @Nullable RequestListener targetListener, @Nullable RequestCoordinator parentCoordinator, TransitionOptions transitionOptions, Priority priority, int overrideWidth, int overrideHeight, BaseRequestOptions requestOptions, Executor callbackExecutor) { // Build the ErrorRequestCoordinator first if necessary so we can update parentCoordinator. ErrorRequestCoordinator errorRequestCoordinator = null; if (errorBuilder != null) { errorRequestCoordinator = new ErrorRequestCoordinator(requestLock, parentCoordinator); parentCoordinator = errorRequestCoordinator; } Request mainRequest = buildThumbnailRequestRecursive( requestLock, target, targetListener, parentCoordinator, transitionOptions, priority, overrideWidth, overrideHeight, requestOptions, callbackExecutor); if (errorRequestCoordinator == null) { return mainRequest; } int errorOverrideWidth = errorBuilder.getOverrideWidth(); int errorOverrideHeight = errorBuilder.getOverrideHeight(); if (Util.isValidDimensions(overrideWidth, overrideHeight) && !errorBuilder.isValidOverride()) { errorOverrideWidth = requestOptions.getOverrideWidth(); errorOverrideHeight = requestOptions.getOverrideHeight(); } Request errorRequest = errorBuilder.buildRequestRecursive( requestLock, target, targetListener, errorRequestCoordinator, errorBuilder.transitionOptions, errorBuilder.getPriority(), errorOverrideWidth, errorOverrideHeight, errorBuilder, callbackExecutor); errorRequestCoordinator.setRequests(mainRequest, errorRequest); return errorRequestCoordinator; } private Request buildThumbnailRequestRecursive( Object requestLock, Target target, RequestListener targetListener, @Nullable RequestCoordinator parentCoordinator, TransitionOptions transitionOptions, Priority priority, int overrideWidth, int overrideHeight, BaseRequestOptions requestOptions, Executor callbackExecutor) { if (thumbnailBuilder != null) { // Recursive case: contains a potentially recursive thumbnail request builder. if (isThumbnailBuilt) { throw new IllegalStateException( "You cannot use a request as both the main request and a " + "thumbnail, consider using clone() on the request(s) passed to thumbnail()"); } TransitionOptions thumbTransitionOptions = thumbnailBuilder.transitionOptions; // Apply our transition by default to thumbnail requests but avoid overriding custom options // that may have been applied on the thumbnail request explicitly. if (thumbnailBuilder.isDefaultTransitionOptionsSet) { thumbTransitionOptions = transitionOptions; } Priority thumbPriority = thumbnailBuilder.isPrioritySet() ? thumbnailBuilder.getPriority() : getThumbnailPriority(priority); int thumbOverrideWidth = thumbnailBuilder.getOverrideWidth(); int thumbOverrideHeight = thumbnailBuilder.getOverrideHeight(); if (Util.isValidDimensions(overrideWidth, overrideHeight) && !thumbnailBuilder.isValidOverride()) { thumbOverrideWidth = requestOptions.getOverrideWidth(); thumbOverrideHeight = requestOptions.getOverrideHeight(); } ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(requestLock, parentCoordinator); Request fullRequest = obtainRequest( requestLock, target, targetListener, requestOptions, coordinator, transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor); isThumbnailBuilt = true; // Recursively generate thumbnail requests. Request thumbRequest = thumbnailBuilder.buildRequestRecursive( requestLock, target, targetListener, coordinator, thumbTransitionOptions, thumbPriority, thumbOverrideWidth, thumbOverrideHeight, thumbnailBuilder, callbackExecutor); isThumbnailBuilt = false; coordinator.setRequests(fullRequest, thumbRequest); return coordinator; } else if (thumbSizeMultiplier != null) { // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse. ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(requestLock, parentCoordinator); Request fullRequest = obtainRequest( requestLock, target, targetListener, requestOptions, coordinator, transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor); BaseRequestOptions thumbnailOptions = requestOptions.clone().sizeMultiplier(thumbSizeMultiplier); Request thumbnailRequest = obtainRequest( requestLock, target, targetListener, thumbnailOptions, coordinator, transitionOptions, getThumbnailPriority(priority), overrideWidth, overrideHeight, callbackExecutor); coordinator.setRequests(fullRequest, thumbnailRequest); return coordinator; } else { // Base case: no thumbnail. return obtainRequest( requestLock, target, targetListener, requestOptions, parentCoordinator, transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor); } } private Request obtainRequest( Object requestLock, Target target, RequestListener targetListener, BaseRequestOptions requestOptions, RequestCoordinator requestCoordinator, TransitionOptions transitionOptions, Priority priority, int overrideWidth, int overrideHeight, Executor callbackExecutor) { return SingleRequest.obtain( context, glideContext, requestLock, model, transcodeClass, requestOptions, overrideWidth, overrideHeight, priority, target, targetListener, requestListeners, requestCoordinator, glideContext.getEngine(), transitionOptions.getTransitionFactory(), callbackExecutor); } Object getModel() { return model; } @Override public boolean equals(Object o) { if (o instanceof RequestBuilder) { RequestBuilder that = (RequestBuilder) o; return super.equals(that) && Objects.equals(transcodeClass, that.transcodeClass) && transitionOptions.equals(that.transitionOptions) && Objects.equals(model, that.model) && Objects.equals(requestListeners, that.requestListeners) && Objects.equals(thumbnailBuilder, that.thumbnailBuilder) && Objects.equals(errorBuilder, that.errorBuilder) && Objects.equals(thumbSizeMultiplier, that.thumbSizeMultiplier) && isDefaultTransitionOptionsSet == that.isDefaultTransitionOptionsSet && isModelSet == that.isModelSet; } return false; } @Override public int hashCode() { int hashCode = super.hashCode(); hashCode = Util.hashCode(transcodeClass, hashCode); hashCode = Util.hashCode(transitionOptions, hashCode); hashCode = Util.hashCode(model, hashCode); hashCode = Util.hashCode(requestListeners, hashCode); hashCode = Util.hashCode(thumbnailBuilder, hashCode); hashCode = Util.hashCode(errorBuilder, hashCode); hashCode = Util.hashCode(thumbSizeMultiplier, hashCode); hashCode = Util.hashCode(isDefaultTransitionOptionsSet, hashCode); hashCode = Util.hashCode(isModelSet, hashCode); return hashCode; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/RequestManager.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.request.RequestOptions.decodeTypeOf; import static com.bumptech.glide.request.RequestOptions.diskCacheStrategyOf; import static com.bumptech.glide.request.RequestOptions.skipMemoryCacheOf; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.view.View; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.manager.ConnectivityMonitor; import com.bumptech.glide.manager.ConnectivityMonitorFactory; import com.bumptech.glide.manager.Lifecycle; import com.bumptech.glide.manager.LifecycleListener; import com.bumptech.glide.manager.RequestManagerTreeNode; import com.bumptech.glide.manager.RequestTracker; import com.bumptech.glide.manager.TargetTracker; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.CustomViewTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.io.File; import java.net.URL; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** * A class for managing and starting requests for Glide. Can use activity, fragment and connectivity * lifecycle events to intelligently stop, start, and restart requests. Retrieve either by * instantiating a new object, or to take advantage built in Activity and Fragment lifecycle * handling, use the static Glide.load methods with your Fragment or Activity. * * @see Glide#with(android.app.Activity) * @see Glide#with(androidx.fragment.app.FragmentActivity) * @see Glide#with(android.app.Fragment) * @see Glide#with(androidx.fragment.app.Fragment) * @see Glide#with(Context) */ public class RequestManager implements ComponentCallbacks2, LifecycleListener, ModelTypes> { private static final RequestOptions DECODE_TYPE_BITMAP = decodeTypeOf(Bitmap.class).lock(); private static final RequestOptions DECODE_TYPE_GIF = decodeTypeOf(GifDrawable.class).lock(); private static final RequestOptions DOWNLOAD_ONLY_OPTIONS = diskCacheStrategyOf(DiskCacheStrategy.DATA).priority(Priority.LOW).skipMemoryCache(true); protected final Glide glide; protected final Context context; @SuppressWarnings("WeakerAccess") @Synthetic final Lifecycle lifecycle; @GuardedBy("this") private final RequestTracker requestTracker; @GuardedBy("this") private final RequestManagerTreeNode treeNode; @GuardedBy("this") private final TargetTracker targetTracker = new TargetTracker(); private final Runnable addSelfToLifecycle = new Runnable() { @Override public void run() { lifecycle.addListener(RequestManager.this); } }; private final ConnectivityMonitor connectivityMonitor; // Adding default listeners should be much less common than starting new requests. We want // some way of making sure that requests don't mutate our listeners without creating a new copy of // the list each time a request is started. private final CopyOnWriteArrayList> defaultRequestListeners; @GuardedBy("this") private RequestOptions requestOptions; private boolean pauseAllRequestsOnTrimMemoryModerate; private boolean clearOnStop; public RequestManager( @NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode treeNode, @NonNull Context context) { this( glide, lifecycle, treeNode, new RequestTracker(), glide.getConnectivityMonitorFactory(), context); } // Our usage is safe here. @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") RequestManager( Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode, RequestTracker requestTracker, ConnectivityMonitorFactory factory, Context context) { this.glide = glide; this.lifecycle = lifecycle; this.treeNode = treeNode; this.requestTracker = requestTracker; this.context = context; connectivityMonitor = factory.build( context.getApplicationContext(), new RequestManagerConnectivityListener(requestTracker)); // Order matters, this might be unregistered by teh listeners below, so we need to be sure to // register first to prevent both assertions and memory leaks. glide.registerRequestManager(this); // If we're the application level request manager, we may be created on a background thread. // In that case we cannot risk synchronously pausing or resuming requests, so we hack around the // issue by delaying adding ourselves as a lifecycle listener by posting to the main thread. // This should be entirely safe. if (Util.isOnBackgroundThread()) { Util.postOnUiThread(addSelfToLifecycle); } else { lifecycle.addListener(this); } lifecycle.addListener(connectivityMonitor); defaultRequestListeners = new CopyOnWriteArrayList<>(glide.getGlideContext().getDefaultRequestListeners()); setRequestOptions(glide.getGlideContext().getDefaultRequestOptions()); } protected synchronized void setRequestOptions(@NonNull RequestOptions toSet) { requestOptions = toSet.clone().autoClone(); } private synchronized void updateRequestOptions(@NonNull RequestOptions toUpdate) { requestOptions = requestOptions.apply(toUpdate); } /** * Updates the default {@link RequestOptions} for all loads started with this request manager with * the given {@link RequestOptions}. * *

    The {@link RequestOptions} provided here are applied on top of those provided via {@link * GlideBuilder#setDefaultRequestOptions(RequestOptions)}. If there are conflicts, the options * applied here will win. Note that this method does not mutate options provided to {@link * GlideBuilder#setDefaultRequestOptions(RequestOptions)}. * *

    Multiple sets of options can be applied. If there are conflicts the last {@link * RequestOptions} applied will win. * *

    The modified options will only be applied to loads started after this method is called. * * @see RequestBuilder#apply(BaseRequestOptions) * @return This request manager. */ @NonNull public synchronized RequestManager applyDefaultRequestOptions( @NonNull RequestOptions requestOptions) { updateRequestOptions(requestOptions); return this; } /** * Replaces the default {@link RequestOptions} for all loads started with this request manager * with the given {@link RequestOptions}. * *

    The {@link RequestOptions} provided here replace those that have been previously provided * via this method, {@link GlideBuilder#setDefaultRequestOptions(RequestOptions)}, and {@link * #applyDefaultRequestOptions(RequestOptions)}. * *

    Subsequent calls to {@link #applyDefaultRequestOptions(RequestOptions)} will not mutate the * {@link RequestOptions} provided here. Instead the manager will create a clone of these options * and mutate the clone. * * @see #applyDefaultRequestOptions(RequestOptions) * @return This request manager. */ @NonNull public synchronized RequestManager setDefaultRequestOptions( @NonNull RequestOptions requestOptions) { setRequestOptions(requestOptions); return this; } /** * Clear all resources when onStop() from {@link LifecycleListener} is called. * * @return This request manager. */ @NonNull public synchronized RequestManager clearOnStop() { clearOnStop = true; return this; } /** * Adds a default {@link RequestListener} that will be added to every request started with this * {@link RequestManager}. * *

    Multiple {@link RequestListener}s can be added here, in {@link RequestManager} scopes or to * individual {@link RequestBuilder}s. {@link RequestListener}s are called in the order they're * added. Even if an earlier {@link RequestListener} returns {@code true} from {@link * RequestListener#onLoadFailed(GlideException, Object, Target, boolean)} or {@link * RequestListener#onResourceReady(Object, Object, Target, DataSource, boolean)}, it will not * prevent subsequent {@link RequestListener}s from being called. * *

    Because Glide requests can be started for any number of individual resource types, any * listener added here has to accept any generic resource type in {@link * RequestListener#onResourceReady(Object, Object, Target, DataSource, boolean)}. If you must base * the behavior of the listener on the resource type, you will need to use {@code instanceof} to * do so. It's not safe to cast resource types without first checking with {@code instanceof}. */ public RequestManager addDefaultRequestListener(RequestListener requestListener) { defaultRequestListeners.add(requestListener); return this; } /** * If {@code true} then clear all in-progress and completed requests when the platform sends * {@code onTrimMemory} with level = {@code TRIM_MEMORY_MODERATE}. */ public void setPauseAllRequestsOnTrimMemoryModerate(boolean pauseAllOnTrim) { pauseAllRequestsOnTrimMemoryModerate = pauseAllOnTrim; } /** * Returns true if loads for this {@link RequestManager} are currently paused. * * @see #pauseRequests() * @see #resumeRequests() */ public synchronized boolean isPaused() { return requestTracker.isPaused(); } /** * Cancels any in progress loads, but does not clear resources of completed loads. * *

    Note #{@link #resumeRequests()} must be called for any requests made before or while the * manager is paused to complete. RequestManagers attached to Fragments and Activities * automatically resume onStart(). * * @see #isPaused() * @see #resumeRequests() */ public synchronized void pauseRequests() { requestTracker.pauseRequests(); } /** * Cancels any in progress loads and clears resources of completed loads. * *

    Note #{@link #resumeRequests()} must be called for any requests made before or while the * manager is paused to complete. RequestManagers attached to Fragments and Activities * automatically resume onStart(). * *

    This will release the memory used by completed bitmaps but leaves them in any configured * caches. When an #{@link android.app.Activity} receives #{@link * android.app.Activity#onTrimMemory(int)} at a level of #{@link * android.content.ComponentCallbacks2#TRIM_MEMORY_BACKGROUND} this is desirable in order to keep * your process alive longer. * * @see #isPaused() * @see #resumeRequests() */ public synchronized void pauseAllRequests() { requestTracker.pauseAllRequests(); } /** * Performs {@link #pauseAllRequests()} recursively for all managers that are contextually * descendant to this manager based on the Activity/Fragment hierarchy. * *

    Similar to {@link #pauseRequestsRecursive()} with the exception that it also clears * resources of completed loads. */ // Public API. @SuppressWarnings({"WeakerAccess", "unused"}) public synchronized void pauseAllRequestsRecursive() { pauseAllRequests(); for (RequestManager requestManager : treeNode.getDescendants()) { requestManager.pauseAllRequests(); } } /** * Performs {@link #pauseRequests()} recursively for all managers that are contextually descendant * to this manager based on the Activity/Fragment hierarchy: * *

      *
    • When pausing on an Activity all attached fragments will also get paused. *
    • When pausing on an attached Fragment all descendant fragments will also get paused. *
    • When pausing on a detached Fragment or the application context only the current * RequestManager is paused. *
    * *

    Note, on pre-Jelly Bean MR1 calling pause on a Fragment will not cause child fragments to * pause, in this case either call pause on the Activity or use a support Fragment. */ // Public API. @SuppressWarnings({"WeakerAccess", "unused"}) public synchronized void pauseRequestsRecursive() { pauseRequests(); for (RequestManager requestManager : treeNode.getDescendants()) { requestManager.pauseRequests(); } } /** * Restarts any loads that have not yet completed. * * @see #isPaused() * @see #pauseRequests() */ public synchronized void resumeRequests() { requestTracker.resumeRequests(); } /** * Performs {@link #resumeRequests()} recursively for all managers that are contextually * descendant to this manager based on the Activity/Fragment hierarchy. The hierarchical semantics * are identical as for {@link #pauseRequestsRecursive()}. */ // Public API. @SuppressWarnings("unused") public synchronized void resumeRequestsRecursive() { Util.assertMainThread(); resumeRequests(); for (RequestManager requestManager : treeNode.getDescendants()) { requestManager.resumeRequests(); } } /** * Lifecycle callback that registers for connectivity events (if the * android.permission.ACCESS_NETWORK_STATE permission is present) and restarts failed or paused * requests. */ @Override public synchronized void onStart() { resumeRequests(); targetTracker.onStart(); } /** * Lifecycle callback that unregisters for connectivity events (if the * android.permission.ACCESS_NETWORK_STATE permission is present) and pauses in progress loads and * clears all resources if {@link #clearOnStop()} is called. */ @Override public synchronized void onStop() { targetTracker.onStop(); if (clearOnStop) { clearRequests(); } else { pauseRequests(); } } /** * Lifecycle callback that cancels all in progress requests and clears and recycles resources for * all completed requests. */ @Override public synchronized void onDestroy() { targetTracker.onDestroy(); clearRequests(); requestTracker.clearRequests(); lifecycle.removeListener(this); lifecycle.removeListener(connectivityMonitor); Util.removeCallbacksOnUiThread(addSelfToLifecycle); glide.unregisterRequestManager(this); } /** * Attempts to always load the resource as a {@link android.graphics.Bitmap}, even if it could * actually be animated. * * @return A new request builder for loading a {@link android.graphics.Bitmap} */ @NonNull @CheckResult public RequestBuilder asBitmap() { return as(Bitmap.class).apply(DECODE_TYPE_BITMAP); } /** * Attempts to always load the resource as a {@link * com.bumptech.glide.load.resource.gif.GifDrawable}. * *

    If the underlying data is not a GIF, this will fail. As a result, this should only be used * if the model represents an animated GIF and the caller wants to interact with the GifDrawable * directly. Normally using just {@link #asDrawable()} is sufficient because it will determine * whether or not the given data represents an animated GIF and return the appropriate {@link * Drawable}, animated or not, automatically. * * @return A new request builder for loading a {@link * com.bumptech.glide.load.resource.gif.GifDrawable}. */ @NonNull @CheckResult public RequestBuilder asGif() { return as(GifDrawable.class).apply(DECODE_TYPE_GIF); } /** * Attempts to always load the resource using any registered {@link * com.bumptech.glide.load.ResourceDecoder}s that can decode any subclass of {@link Drawable}. * *

    By default, may return either a {@link android.graphics.drawable.BitmapDrawable} or {@link * GifDrawable}, but if additional decoders are registered for other {@link Drawable} subclasses, * any of those subclasses may also be returned. * * @return A new request builder for loading a {@link Drawable}. */ @NonNull @CheckResult public RequestBuilder asDrawable() { return as(Drawable.class); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(Bitmap)}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable Bitmap bitmap) { return asDrawable().load(bitmap); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(Drawable)}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable Drawable drawable) { return asDrawable().load(drawable); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(String)}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable String string) { return asDrawable().load(string); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(Uri)}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable Uri uri) { return asDrawable().load(uri); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(File)}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable File file) { return asDrawable().load(file); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(Integer)}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @SuppressWarnings("deprecation") @NonNull @CheckResult @Override public RequestBuilder load(@RawRes @DrawableRes @Nullable Integer resourceId) { return asDrawable().load(resourceId); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(URL)}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @SuppressWarnings("deprecation") @CheckResult @Override @Deprecated public RequestBuilder load(@Nullable URL url) { return asDrawable().load(url); } /** * Equivalent to calling {@link #asDrawable()} and then {@link RequestBuilder#load(byte[])}. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable byte[] model) { return asDrawable().load(model); } /** * A helper method equivalent to calling {@link #asDrawable()} and then {@link * RequestBuilder#load(Object)} with the given model. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult @Override public RequestBuilder load(@Nullable Object model) { return asDrawable().load(model); } /** * Attempts always load the resource into the cache and return the {@link File} containing the * cached source data. * *

    This method is designed to work for remote data that is or will be cached using {@link * com.bumptech.glide.load.engine.DiskCacheStrategy#DATA}. As a result, specifying a {@link * com.bumptech.glide.load.engine.DiskCacheStrategy} on this request is generally not recommended. * * @return A new request builder for downloading content to cache and returning the cache File. */ @NonNull @CheckResult public RequestBuilder downloadOnly() { return as(File.class).apply(DOWNLOAD_ONLY_OPTIONS); } /** * A helper method equivalent to calling {@link #downloadOnly()} ()} and then {@link * RequestBuilder#load(Object)} with the given model. * * @return A new request builder for loading a {@link Drawable} using the given model. */ @NonNull @CheckResult public RequestBuilder download(@Nullable Object model) { return downloadOnly().load(model); } /** * Attempts to always load a {@link File} containing the resource, either using a file path * obtained from the media store (for local images/videos), or using Glide's disk cache (for * remote images/videos). * *

    For remote content, prefer {@link #downloadOnly()}. * * @return A new request builder for obtaining File paths to content. */ @NonNull @CheckResult public RequestBuilder asFile() { return as(File.class).apply(skipMemoryCacheOf(true)); } /** * Attempts to load the resource using any registered {@link * com.bumptech.glide.load.ResourceDecoder}s that can decode the given resource class or any * subclass of the given resource class. * * @param resourceClass The resource to decode. * @return A new request builder for loading the given resource class. */ @NonNull @CheckResult public RequestBuilder as( @NonNull Class resourceClass) { return new RequestBuilder<>(glide, this, resourceClass, context); } /** * Cancel any pending loads Glide may have for the view and free any resources that may have been * loaded for the view. * *

    Note that this will only work if {@link View#setTag(Object)} is not called on this view * outside of Glide. * * @param view The view to cancel loads and free resources for. * @throws IllegalArgumentException if an object other than Glide's metadata is put as the view's * tag. * @see #clear(Target) */ public void clear(@NonNull View view) { clear(new ClearTarget(view)); } /** * Cancel any pending loads Glide may have for the target and free any resources (such as {@link * Bitmap}s) that may have been loaded for the target so they may be reused. * * @param target The Target to cancel loads for. */ public void clear(@Nullable final Target target) { if (target == null) { return; } untrackOrDelegate(target); } private void untrackOrDelegate(@NonNull Target target) { boolean isOwnedByUs = untrack(target); // We'll end up here if the Target was cleared after the RequestManager that started the request // is destroyed. That can happen for at least two reasons: // 1. We call clear() on a background thread using something other than Application Context // RequestManager. // 2. The caller retains a reference to the RequestManager after the corresponding Activity or // Fragment is destroyed, starts a load with it, and then clears that load with a different // RequestManager. Callers seem especially likely to do this in retained Fragments (#2262). // // #1 is always an error. At best the caller is leaking memory briefly in something like an // AsyncTask. At worst the caller is leaking an Activity or Fragment for a sustained period of // time if they do something like reference the Activity RequestManager in a long lived // background thread or task. // // #2 is always an error. Callers shouldn't be starting new loads using RequestManagers after // the corresponding Activity or Fragment is destroyed because retaining any reference to the // RequestManager leaks memory. It's possible that there's some brief period of time during or // immediately after onDestroy where this is reasonable, but I can't think of why. Request request = target.getRequest(); if (!isOwnedByUs && !glide.removeFromManagers(target) && request != null) { target.setRequest(null); request.clear(); } } synchronized boolean untrack(@NonNull Target target) { Request request = target.getRequest(); // If the Target doesn't have a request, it's already been cleared. if (request == null) { return true; } if (requestTracker.clearAndRemove(request)) { targetTracker.untrack(target); target.setRequest(null); return true; } else { return false; } } synchronized void track(@NonNull Target target, @NonNull Request request) { targetTracker.track(target); requestTracker.runRequest(request); } List> getDefaultRequestListeners() { return defaultRequestListeners; } synchronized RequestOptions getDefaultRequestOptions() { return requestOptions; } @NonNull TransitionOptions getDefaultTransitionOptions(Class transcodeClass) { return glide.getGlideContext().getDefaultTransitionOptions(transcodeClass); } @Override public synchronized String toString() { return super.toString() + "{tracker=" + requestTracker + ", treeNode=" + treeNode + "}"; } @Override public void onTrimMemory(int level) { if (level == TRIM_MEMORY_MODERATE && pauseAllRequestsOnTrimMemoryModerate) { pauseAllRequestsRecursive(); } } @Override public void onLowMemory() { // Nothing to add conditionally. See Glide#onTrimMemory for unconditional behavior. } private synchronized void clearRequests() { for (Target target : targetTracker.getAll()) { clear(target); } targetTracker.clear(); } @Override public void onConfigurationChanged(Configuration newConfig) {} private class RequestManagerConnectivityListener implements ConnectivityMonitor.ConnectivityListener { @GuardedBy("RequestManager.this") private final RequestTracker requestTracker; RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) { this.requestTracker = requestTracker; } @Override public void onConnectivityChanged(boolean isConnected) { if (isConnected) { synchronized (RequestManager.this) { requestTracker.restartRequests(); } } } } private static class ClearTarget extends CustomViewTarget { ClearTarget(@NonNull View view) { super(view); } @Override protected void onResourceCleared(@Nullable Drawable placeholder) { // Do nothing, we don't retain a reference to our resource. } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { // Do nothing. } @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/TransitionOptions.java ================================================ package com.bumptech.glide; import androidx.annotation.NonNull; import com.bumptech.glide.request.transition.NoTransition; import com.bumptech.glide.request.transition.TransitionFactory; import com.bumptech.glide.request.transition.ViewAnimationFactory; import com.bumptech.glide.request.transition.ViewPropertyAnimationFactory; import com.bumptech.glide.request.transition.ViewPropertyTransition; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; /** * A base class for setting a transition to use on a resource when a load completes. * *

    Note: Implementations must implement equals/hashcode. * * @param The implementation of this class to return to chain methods. * @param The type of resource that will be animated. */ public abstract class TransitionOptions< CHILD extends TransitionOptions, TranscodeType> implements Cloneable { private TransitionFactory transitionFactory = NoTransition.getFactory(); /** * Removes any existing animation put on the builder. Will be overridden by subsequent calls that * put an animation. * * @return This request builder. */ @NonNull public final CHILD dontTransition() { return transition(NoTransition.getFactory()); } /** * Sets an {@link android.view.animation.Animation} to run on the wrapped target when an resource * load finishes. Will only be run if the resource was loaded asynchronously (i.e. was not in the * memory cache). * * @param viewAnimationId The resource id of the {@link android.view.animation.Animation} to use * as the transition. * @return This request builder. */ @NonNull public final CHILD transition(int viewAnimationId) { return transition(new ViewAnimationFactory<>(viewAnimationId)); } /** * Sets an animator to run a {@link android.view.ViewPropertyAnimator} on a view that the target * may be wrapping when a resource load finishes. Will only be run if the load was loaded * asynchronously (i.e. was not in the memory cache). * * @param animator The {@link com.bumptech.glide.request.transition.ViewPropertyTransition * .Animator} to run. * @return This request builder. */ @NonNull public final CHILD transition(@NonNull ViewPropertyTransition.Animator animator) { return transition(new ViewPropertyAnimationFactory<>(animator)); } /** * Uses the given {@link TransitionFactory} to build a {@link * com.bumptech.glide.request.transition.Transition} for each request started with these {@code * TransitionOptions}. * * @return This request builder. */ @NonNull public final CHILD transition( @NonNull TransitionFactory transitionFactory) { this.transitionFactory = Preconditions.checkNotNull(transitionFactory); return self(); } @SuppressWarnings({ // cast to CHILD is safe given the generic argument represents the object's runtime class "unchecked", // CHILD is the correct class name. "PMD.CloneMethodReturnTypeMustMatchClassName", // we don't want to throw to be user friendly "PMD.CloneThrowsCloneNotSupportedException" }) @Override public final CHILD clone() { try { return (CHILD) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } final TransitionFactory getTransitionFactory() { return transitionFactory; } @SuppressWarnings("unchecked") private CHILD self() { return (CHILD) this; } @Override public boolean equals(Object o) { if (o instanceof TransitionOptions) { TransitionOptions other = (TransitionOptions) o; return Util.bothNullOrEqual(transitionFactory, other.transitionFactory); } return false; } @Override public int hashCode() { return transitionFactory != null ? transitionFactory.hashCode() : 0; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/DataSource.java ================================================ package com.bumptech.glide.load; /** Indicates the origin of some retrieved data. */ public enum DataSource { /** * Indicates data was probably retrieved locally from the device, although it may have been * obtained through a content provider that may have obtained the data from a remote source. */ LOCAL, /** Indicates data was retrieved from a remote source other than the device. */ REMOTE, /** Indicates data was retrieved unmodified from the on device cache. */ DATA_DISK_CACHE, /** Indicates data was retrieved from modified content in the on device cache. */ RESOURCE_DISK_CACHE, /** Indicates data was retrieved from the in memory cache. */ MEMORY_CACHE, } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/DecodeFormat.java ================================================ package com.bumptech.glide.load; /** * Options for setting the value of {@link android.graphics.Bitmap#getConfig()} for {@link * android.graphics.Bitmap}s returned by {@link com.bumptech.glide.load.ResourceDecoder}s. * *

    Note - In some cases it may not be possible to obey the requested setting, not all {@link * com.bumptech.glide.load.resource.bitmap.Downsampler}s support setting formats and certain images * may not be able to be loaded as certain configurations. Therefore this class represents a * preference rather than a requirement. */ public enum DecodeFormat { /** * Bitmaps returned by the {@link com.bumptech.glide.load.ResourceDecoder}. should return {@link * android.graphics.Bitmap.Config#ARGB_8888} for {@link android.graphics.Bitmap#getConfig()} when * possible. * *

    On Android O+, this format will use ARGB_8888 only when it's not possible to use {@link * android.graphics.Bitmap.Config#HARDWARE}. More information is available about hardware Bitmaps * here: https://goo.gl/tn2A6k. If you need to disable hardware Bitmaps for a particular request, * use {@link com.bumptech.glide.request.RequestOptions#disallowHardwareConfig()}. * *

    GIF images decoded by {@link android.graphics.BitmapFactory} currently use an internal * hidden format that is returned as null from {@link android.graphics.Bitmap#getConfig()}. Since * we cannot force {@link android.graphics.BitmapFactory} to always return our desired config, * this setting is a preference, not a promise. */ PREFER_ARGB_8888, /** * Bitmaps decoded from image formats that support and/or use alpha (some types of PNGs, GIFs etc) * should return {@link android.graphics.Bitmap.Config#ARGB_8888} for {@link * android.graphics.Bitmap#getConfig()}. Bitmaps decoded from formats that don't support or use * alpha should return {@link android.graphics.Bitmap.Config#RGB_565} for {@link * android.graphics.Bitmap#getConfig()}. * *

    On Android O+, this format will use RGB_565 only when it's not possible to use {@link * android.graphics.Bitmap.Config#HARDWARE}. */ PREFER_RGB_565; /** The default value for DecodeFormat. */ public static final DecodeFormat DEFAULT = PREFER_ARGB_8888; } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/EncodeStrategy.java ================================================ package com.bumptech.glide.load; /** * Details how an {@link com.bumptech.glide.load.ResourceEncoder} will encode a resource to cache. */ public enum EncodeStrategy { /** * Writes the original unmodified data for the resource to disk, not include downsampling or * transformations. */ SOURCE, /** Writes the decoded, downsampled and transformed data for the resource to disk. */ TRANSFORMED, /** Will write no data. */ NONE, } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/Encoder.java ================================================ package com.bumptech.glide.load; import androidx.annotation.NonNull; import java.io.File; /** * An interface for writing data to some persistent data store (i.e. a local File cache). * * @param The type of the data that will be written. */ public interface Encoder { /** * Writes the given data to the given output stream and returns True if the write completed * successfully and should be committed. * * @param data The data to write. * @param file The file to write the data to. * @param options The set of options to apply when encoding. */ boolean encode(@NonNull T data, @NonNull File file, @NonNull Options options); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/HttpException.java ================================================ package com.bumptech.glide.load; import androidx.annotation.Nullable; import java.io.IOException; /** * Thrown when an http request fails. * *

    Exposes the specific status code or {@link #UNKNOWN} via {@link #getStatusCode()} so users may * attempt to retry or otherwise uniformly handle certain types of errors regardless of the * underlying http library. */ // Public API. @SuppressWarnings({"WeakerAccess", "unused"}) public final class HttpException extends IOException { private static final long serialVersionUID = 1L; public static final int UNKNOWN = -1; private final int statusCode; public HttpException(int statusCode) { this("Http request failed", statusCode); } /** * @deprecated You should always include a status code, default to {@link #UNKNOWN} if you can't * come up with a reasonable one. This method will be removed in a future version. */ @Deprecated public HttpException(String message) { this(message, UNKNOWN); } public HttpException(String message, int statusCode) { this(message, statusCode, null /*cause*/); } public HttpException(String message, int statusCode, @Nullable Throwable cause) { super(message + ", status code: " + statusCode, cause); this.statusCode = statusCode; } /** * Returns the http status code, or {@link #UNKNOWN} if the request failed without providing a * status code. */ public int getStatusCode() { return statusCode; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java ================================================ package com.bumptech.glide.load; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; /** Interface for the ImageHeaderParser. */ public interface ImageHeaderParser { /** * A constant indicating we were unable to parse the orientation from the image either because no * exif segment containing orientation data existed, or because of an I/O error attempting to read * the exif segment. */ int UNKNOWN_ORIENTATION = -1; /** * The format of the image data including whether or not the image may include transparent pixels. */ enum ImageType { GIF(true), JPEG(false), RAW(false), /** PNG type with alpha. */ PNG_A(true), /** PNG type without alpha. */ PNG(false), /** WebP type with alpha. */ WEBP_A(true), /** WebP type without alpha. */ WEBP(false), /** All animated webps. */ ANIMATED_WEBP(true), /** Avif type (may contain alpha). */ AVIF(true), /** Animated Avif type (may contain alpha). */ ANIMATED_AVIF(true), /** Unrecognized type. */ UNKNOWN(false); private final boolean hasAlpha; ImageType(boolean hasAlpha) { this.hasAlpha = hasAlpha; } public boolean hasAlpha() { return hasAlpha; } public boolean isWebp() { switch (this) { case WEBP: case WEBP_A: case ANIMATED_WEBP: return true; default: return false; } } } @NonNull ImageType getType(@NonNull InputStream is) throws IOException; @NonNull ImageType getType(@NonNull ByteBuffer byteBuffer) throws IOException; /** * Parse the orientation from the image header. If it doesn't handle this image type (or this is * not an image) it will return a default value rather than throwing an exception. * * @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't * contain an orientation */ int getOrientation(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException; int getOrientation(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException; /** * Returns whether the {@link InputStream} has associated multi-picture-format (MPF) data. Only * JPEGs have MPF data. */ boolean hasJpegMpf(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException; /** * Returns whether the {@link ByteBuffer} has associated multi-picture-format (MPF) data. Only * JPEGs have MPF data. */ boolean hasJpegMpf(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException; } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/ImageHeaderParserUtils.java ================================================ package com.bumptech.glide.load; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream; import com.bumptech.glide.util.ByteBufferUtil; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.List; /** Utilities for the ImageHeaderParser. */ public final class ImageHeaderParserUtils { // 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer but // will resize up to this amount if necessary. private static final int MARK_READ_LIMIT = 5 * 1024 * 1024; private ImageHeaderParserUtils() {} /** Returns the ImageType for the given InputStream. */ @NonNull public static ImageType getType( @NonNull List parsers, @Nullable InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { if (is == null) { return ImageType.UNKNOWN; } if (!is.markSupported()) { is = new RecyclableBufferedInputStream(is, byteArrayPool); } is.mark(MARK_READ_LIMIT); final InputStream finalIs = is; return getTypeInternal( parsers, new TypeReader() { @Override public ImageType getTypeAndRewind(ImageHeaderParser parser) throws IOException { try { return parser.getType(finalIs); } finally { finalIs.reset(); } } }); } /** Returns the ImageType for the given ByteBuffer. */ @NonNull public static ImageType getType( @NonNull List parsers, @Nullable final ByteBuffer buffer) throws IOException { if (buffer == null) { return ImageType.UNKNOWN; } return getTypeInternal( parsers, new TypeReader() { @Override public ImageType getTypeAndRewind(ImageHeaderParser parser) throws IOException { try { return parser.getType(buffer); } finally { ByteBufferUtil.rewind(buffer); } } }); } @NonNull @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public static ImageType getType( @NonNull List parsers, @NonNull final ParcelFileDescriptorRewinder parcelFileDescriptorRewinder, @NonNull final ArrayPool byteArrayPool) throws IOException { return getTypeInternal( parsers, new TypeReader() { @Override public ImageType getTypeAndRewind(ImageHeaderParser parser) throws IOException { // Wrap the FileInputStream into a RecyclableBufferedInputStream to optimize I/O // performance RecyclableBufferedInputStream is = null; try { is = new RecyclableBufferedInputStream( new FileInputStream( parcelFileDescriptorRewinder.rewindAndGet().getFileDescriptor()), byteArrayPool); return parser.getType(is); } finally { // If we close the stream, we'll close the file descriptor as well, so we can't do // that. We do however want to make sure we release any buffers we used back to the // pool so we call release instead of close. if (is != null) { is.release(); } parcelFileDescriptorRewinder.rewindAndGet(); } } }); } @NonNull private static ImageType getTypeInternal( @NonNull List parsers, TypeReader reader) throws IOException { //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = parsers.size(); i < size; i++) { ImageHeaderParser parser = parsers.get(i); ImageType type = reader.getTypeAndRewind(parser); if (type != ImageType.UNKNOWN) { return type; } } return ImageType.UNKNOWN; } /** * Returns the result from the first of {@code parsers} that returns something other than {@link * ImageHeaderParser#UNKNOWN_ORIENTATION}. * *

    If {@code buffer} is null, the parers list is empty, or none of the parsers returns a valid * value, {@link ImageHeaderParser#UNKNOWN_ORIENTATION} is returned. */ public static int getOrientation( @NonNull List parsers, @Nullable final ByteBuffer buffer, @NonNull final ArrayPool arrayPool) throws IOException { if (buffer == null) { return ImageHeaderParser.UNKNOWN_ORIENTATION; } return getOrientationInternal( parsers, new OrientationReader() { @Override public int getOrientationAndRewind(ImageHeaderParser parser) throws IOException { try { return parser.getOrientation(buffer, arrayPool); } finally { ByteBufferUtil.rewind(buffer); } } }); } /** Returns the orientation for the given InputStream. */ public static int getOrientation( @NonNull List parsers, @Nullable InputStream is, @NonNull final ArrayPool byteArrayPool) throws IOException { if (is == null) { return ImageHeaderParser.UNKNOWN_ORIENTATION; } if (!is.markSupported()) { is = new RecyclableBufferedInputStream(is, byteArrayPool); } is.mark(MARK_READ_LIMIT); final InputStream finalIs = is; return getOrientationInternal( parsers, new OrientationReader() { @Override public int getOrientationAndRewind(ImageHeaderParser parser) throws IOException { try { return parser.getOrientation(finalIs, byteArrayPool); } finally { finalIs.reset(); } } }); } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public static int getOrientation( @NonNull List parsers, @NonNull final ParcelFileDescriptorRewinder parcelFileDescriptorRewinder, @NonNull final ArrayPool byteArrayPool) throws IOException { return getOrientationInternal( parsers, new OrientationReader() { @Override public int getOrientationAndRewind(ImageHeaderParser parser) throws IOException { // Wrap the FileInputStream into a RecyclableBufferedInputStream to optimize I/O // performance RecyclableBufferedInputStream is = null; try { is = new RecyclableBufferedInputStream( new FileInputStream( parcelFileDescriptorRewinder.rewindAndGet().getFileDescriptor()), byteArrayPool); return parser.getOrientation(is, byteArrayPool); } finally { // If we close the stream, we'll close the file descriptor as well, so we can't do // that. We do however want to make sure we release any buffers we used back to the // pool so we call release instead of close. if (is != null) { is.release(); } parcelFileDescriptorRewinder.rewindAndGet(); } } }); } private static int getOrientationInternal( @NonNull List parsers, OrientationReader reader) throws IOException { //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = parsers.size(); i < size; i++) { ImageHeaderParser parser = parsers.get(i); int orientation = reader.getOrientationAndRewind(parser); if (orientation != ImageHeaderParser.UNKNOWN_ORIENTATION) { return orientation; } } return ImageHeaderParser.UNKNOWN_ORIENTATION; } /** * Returns the result from the first of {@code parsers} that returns true when MPF is detected, if * any.. * *

    If {@code buffer} is null, the parsers list is empty, or none of the parsers returns a valid * value, false is returned. */ public static boolean hasJpegMpf( @NonNull List parsers, @Nullable final ByteBuffer buffer, @NonNull ArrayPool byteArrayPool) throws IOException { if (buffer == null) { return false; } return hasJpegMpfInternal( parsers, new JpegMpfReader() { @Override public boolean getHasJpegMpfAndRewind(ImageHeaderParser parser) throws IOException { try { return parser.hasJpegMpf(buffer, byteArrayPool); } finally { ByteBufferUtil.rewind(buffer); } } }); } /** Returns whether the given {@link InputStream} references MPF. */ public static boolean hasJpegMpf( @NonNull List parsers, @Nullable InputStream is, @NonNull final ArrayPool byteArrayPool) throws IOException { if (is == null) { return false; } if (!is.markSupported()) { is = new RecyclableBufferedInputStream(is, byteArrayPool); } is.mark(MARK_READ_LIMIT); final InputStream finalIs = is; return hasJpegMpfInternal( parsers, new JpegMpfReader() { @Override public boolean getHasJpegMpfAndRewind(ImageHeaderParser parser) throws IOException { try { return parser.hasJpegMpf(finalIs, byteArrayPool); } finally { finalIs.reset(); } } }); } /** Returns whether the given {@link ParcelFileDescriptorRewinder} references MPF. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public static boolean hasJpegMpf( @NonNull List parsers, @NonNull final ParcelFileDescriptorRewinder parcelFileDescriptorRewinder, @NonNull final ArrayPool byteArrayPool) throws IOException { return hasJpegMpfInternal( parsers, new JpegMpfReader() { @Override public boolean getHasJpegMpfAndRewind(ImageHeaderParser parser) throws IOException { // Wrap the FileInputStream into a RecyclableBufferedInputStream to optimize I/O // performance RecyclableBufferedInputStream is = null; try { is = new RecyclableBufferedInputStream( new FileInputStream( parcelFileDescriptorRewinder.rewindAndGet().getFileDescriptor()), byteArrayPool); return parser.hasJpegMpf(is, byteArrayPool); } finally { // If we close the stream, we'll close the file descriptor as well, so we can't do // that. We do however want to make sure we release any buffers we used back to the // pool so we call release instead of close. if (is != null) { is.release(); } parcelFileDescriptorRewinder.rewindAndGet(); } } }); } private static boolean hasJpegMpfInternal( @NonNull List parsers, JpegMpfReader reader) throws IOException { //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = parsers.size(); i < size; i++) { ImageHeaderParser parser = parsers.get(i); if (reader.getHasJpegMpfAndRewind(parser)) { return true; } } return false; } private interface TypeReader { ImageType getTypeAndRewind(ImageHeaderParser parser) throws IOException; } private interface OrientationReader { int getOrientationAndRewind(ImageHeaderParser parser) throws IOException; } /** Reads JPEG multi-picture format (MPF) data. */ private interface JpegMpfReader { /** * Returns whether the image is JPEG and has MPF data. * *

    The parser is guaranteed to be rewound upon termination of the method. */ boolean getHasJpegMpfAndRewind(ImageHeaderParser parser) throws IOException; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/Key.java ================================================ package com.bumptech.glide.load; import androidx.annotation.NonNull; import java.nio.charset.Charset; import java.security.MessageDigest; /** * An interface that uniquely identifies some put of data. Implementations must implement {@link * Object#equals(Object)} and {@link Object#hashCode()}. Implementations are generally expected to * add all uniquely identifying information used in in {@link java.lang.Object#equals(Object)}} and * {@link Object#hashCode()}} to the given {@link java.security.MessageDigest} in {@link * #updateDiskCacheKey(java.security.MessageDigest)}}, although this requirement is not as strict * for partial cache key signatures. */ public interface Key { String STRING_CHARSET_NAME = "UTF-8"; Charset CHARSET = Charset.forName(STRING_CHARSET_NAME); /** * Adds all uniquely identifying information to the given digest. * *

    Note - Using {@link java.security.MessageDigest#reset()} inside of this method will result * in undefined behavior. */ void updateDiskCacheKey(@NonNull MessageDigest messageDigest); /** * For caching to work correctly, implementations must implement this method and {@link * #hashCode()}. */ @Override boolean equals(Object o); /** * For caching to work correctly, implementations must implement this method and {@link * #equals(Object)}. */ @Override int hashCode(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/MultiTransformation.java ================================================ package com.bumptech.glide.load; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.Resource; import java.security.MessageDigest; import java.util.Arrays; import java.util.Collection; /** * A transformation that applies one or more transformations in iteration order to a resource. * * @param The type of {@link com.bumptech.glide.load.engine.Resource} that will be transformed. */ public class MultiTransformation implements Transformation { private final Collection> transformations; @SafeVarargs @SuppressWarnings("varargs") public MultiTransformation(@NonNull Transformation... transformations) { if (transformations.length == 0) { throw new IllegalArgumentException( "MultiTransformation must contain at least one Transformation"); } this.transformations = Arrays.asList(transformations); } public MultiTransformation(@NonNull Collection> transformationList) { if (transformationList.isEmpty()) { throw new IllegalArgumentException( "MultiTransformation must contain at least one Transformation"); } this.transformations = transformationList; } @NonNull @Override public Resource transform( @NonNull Context context, @NonNull Resource resource, int outWidth, int outHeight) { Resource previous = resource; for (Transformation transformation : transformations) { Resource transformed = transformation.transform(context, previous, outWidth, outHeight); if (previous != null && !previous.equals(resource) && !previous.equals(transformed)) { previous.recycle(); } previous = transformed; } return previous; } @Override public boolean equals(Object o) { if (o instanceof MultiTransformation) { MultiTransformation other = (MultiTransformation) o; return transformations.equals(other.transformations); } return false; } @Override public int hashCode() { return transformations.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { for (Transformation transformation : transformations) { transformation.updateDiskCacheKey(messageDigest); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/Option.java ================================================ package com.bumptech.glide.load; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.util.Preconditions; import java.security.MessageDigest; /** * Defines available component (decoders, encoders, model loaders etc.) options with optional * default values and the ability to affect the resource disk cache key used by {@link * com.bumptech.glide.load.engine.DiskCacheStrategy#RESOURCE}. * *

    Implementations must either be unique (usually declared as static final variables), or * implement {@link #equals(Object)} and {@link #hashCode()}. * *

    Implementations can implement {@link #update(Object, MessageDigest)} to make sure that the * disk cache key includes the specific option set. * * @param The type of the option ({@link Integer}, {@link * android.graphics.Bitmap.CompressFormat} etc.), must implement {@link #equals(Object)} and * {@link #hashCode()}. */ public final class Option { private static final CacheKeyUpdater EMPTY_UPDATER = new CacheKeyUpdater() { @Override public void update( @NonNull byte[] keyBytes, @NonNull Object value, @NonNull MessageDigest messageDigest) { // Do nothing. } }; private final T defaultValue; private final CacheKeyUpdater cacheKeyUpdater; private final String key; private volatile byte[] keyBytes; /** * Returns a new {@link Option} that does not affect disk cache keys with a {@code null} default * value. * * @param key A unique package prefixed {@link String} that identifies this option (must be stable * across builds, so {@link Class#getName()} should not be used). */ @NonNull public static Option memory(@NonNull String key) { return new Option<>(key, null, Option.emptyUpdater()); } /** * Returns a new {@link Option} that does not affect disk cache keys with the given value as the * default value. * * @param key A unique package prefixed {@link String} that identifies this option (must be stable * across builds, so {@link Class#getName()} should not be used). */ @NonNull public static Option memory(@NonNull String key, @NonNull T defaultValue) { return new Option<>(key, defaultValue, Option.emptyUpdater()); } /** * Returns a new {@link Option} that uses the given {@link * com.bumptech.glide.load.Option.CacheKeyUpdater} to update disk cache keys. * * @param key A unique package prefixed {@link String} that identifies this option (must be stable * across builds, so {@link Class#getName()} should not be used). */ @NonNull public static Option disk( @NonNull String key, @NonNull CacheKeyUpdater cacheKeyUpdater) { return new Option<>(key, null, cacheKeyUpdater); } /** * Returns a new {@link Option} that uses the given {@link * com.bumptech.glide.load.Option.CacheKeyUpdater} to update disk cache keys and provides the * given value as the default value. * * @param key A unique package prefixed {@link String} that identifies this option (must be stable * across builds, so {@link Class#getName()} should not be used). */ @NonNull public static Option disk( @NonNull String key, @Nullable T defaultValue, @NonNull CacheKeyUpdater cacheKeyUpdater) { return new Option<>(key, defaultValue, cacheKeyUpdater); } private Option( @NonNull String key, @Nullable T defaultValue, @NonNull CacheKeyUpdater cacheKeyUpdater) { this.key = Preconditions.checkNotEmpty(key); this.defaultValue = defaultValue; this.cacheKeyUpdater = Preconditions.checkNotNull(cacheKeyUpdater); } /** Returns a reasonable default to use if no other value is set, or {@code null}. */ // Public API. @SuppressWarnings("WeakerAccess") @Nullable public T getDefaultValue() { return defaultValue; } /** * Updates the given {@link MessageDigest} used to construct a cache key with the given value * using the {@link com.bumptech.glide.load.Option.CacheKeyUpdater} optionally provided in the * constructor. */ public void update(@NonNull T value, @NonNull MessageDigest messageDigest) { cacheKeyUpdater.update(getKeyBytes(), value, messageDigest); } @NonNull private byte[] getKeyBytes() { if (keyBytes == null) { keyBytes = key.getBytes(Key.CHARSET); } return keyBytes; } @Override public boolean equals(Object o) { if (o instanceof Option) { Option other = (Option) o; return key.equals(other.key); } return false; } @Override public int hashCode() { return key.hashCode(); } @NonNull @SuppressWarnings("unchecked") private static CacheKeyUpdater emptyUpdater() { return (CacheKeyUpdater) EMPTY_UPDATER; } @Override public String toString() { return "Option{" + "key='" + key + '\'' + '}'; } /** * An interface that updates a {@link MessageDigest} with the given value as part of a process to * generate a disk cache key. * * @param The type of the option. */ public interface CacheKeyUpdater { /** * Updates the given {@link MessageDigest} with the bytes of the given key (to avoid incidental * value collisions when values are not particularly unique) and value. * *

    If your {@link Option} shouldn't affect the disk cache key, you should not implement this * class and use {@link Option#memory(String)} or {@link Option#memory(String, Object)} instead. * * @param keyBytes The bytes of the {@link String} used as the key for this particular {@link * Option}. Should be added to the {@code messageDigest} using {@link * MessageDigest#update(byte[])} by all implementations if the digest is updated with the * given {@code value} parameter. * @param value The value of of this particular option. Typically you should convert the value * to a byte array using some stable mechanism and then call {@link * MessageDigest#update(byte[])} to update the given digest. */ void update(@NonNull byte[] keyBytes, @NonNull T value, @NonNull MessageDigest messageDigest); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/Options.java ================================================ package com.bumptech.glide.load; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import androidx.collection.SimpleArrayMap; import com.bumptech.glide.util.CachedHashCodeArrayMap; import java.security.MessageDigest; /** A set of {@link Option Options} to apply to in memory and disk cache keys. */ public final class Options implements Key { private final ArrayMap, Object> values = new CachedHashCodeArrayMap<>(); public void putAll(@NonNull Options other) { values.putAll((SimpleArrayMap, Object>) other.values); } @NonNull public Options set(@NonNull Option option, @NonNull T value) { values.put(option, value); return this; } // TODO(b/234614365): Expand usage of this method in BaseRequestOptions so that it's usable for // other options. public Options remove(@NonNull Option option) { values.remove(option); return this; } @Nullable @SuppressWarnings("unchecked") public T get(@NonNull Option option) { return values.containsKey(option) ? (T) values.get(option) : option.getDefaultValue(); } @Override public boolean equals(Object o) { if (o instanceof Options) { Options other = (Options) o; return values.equals(other.values); } return false; } @Override public int hashCode() { return values.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { for (int i = 0; i < values.size(); i++) { Option key = values.keyAt(i); Object value = values.valueAt(i); updateDiskCacheKey(key, value, messageDigest); } } @Override public String toString() { return "Options{" + "values=" + values + '}'; } @SuppressWarnings("unchecked") private static void updateDiskCacheKey( @NonNull Option option, @NonNull Object value, @NonNull MessageDigest md) { option.update((T) value, md); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/PreferredColorSpace.java ================================================ package com.bumptech.glide.load; /** * Glide's supported handling of color spaces on Android O+, defaults to null. * *

    On Android O, Glide will always request SRGB and will ignore this option if set. A bug on * Android O prevents P3 images from being compressed correctly and can result in color distortion. * We may eventually work around this in Glide if sufficient demand arises, but doing so will * require a memory intensive conversion to SRGB prior to compressing Bitmaps to Glide's disk cache. * This work around would also work only for Glide's compression, not for any compression that a * caller performs on a Bitmap returned by Glide. * *

    On Android P+, Glide supports SRGB and display P3. However, if display p3 is requested, we * will still decode to SRGB unless {@link android.graphics.BitmapFactory.Options#outColorSpace} is * also {@link android.graphics.ColorSpace.Named#DISPLAY_P3}. Preferring P3 for SRGB images adds * unnecessary CPU work to convert back and forth between the color spaces at decode time. * *

    Using {@link #DISPLAY_P3} is wasteful if either the screen or the renderer do not support P3. * Currently Glide does not attempt to detect whether or not this support is present. Do not use * {@link #DISPLAY_P3} thinking that you're going to get higher quality by default. Only use {@link * #DISPLAY_P3} if you're confident you understand color spaces, your application is working with a * display that supports wide gamut and you've set the appropriate options to render wide gamut * colors. If you've missed one or more of these steps, {@link #DISPLAY_P3} can lead to poor color * quality and washed out looking images. When in doubt, always use {@link #SRGB}, which is Glide's * default. * *

    As with {@link DecodeFormat} we cannot directly set color spaces, we can only suggest to the * framework which one we want. Setting one of these values is not a guarantee that any returned * Bitmap will actually use the requested color space. */ public enum PreferredColorSpace { /** Prefers to decode images using {@link android.graphics.ColorSpace.Named#SRGB}. */ SRGB, /** Prefers to decode images using {@link android.graphics.ColorSpace.Named#DISPLAY_P3}. */ DISPLAY_P3, } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/ResourceDecoder.java ================================================ package com.bumptech.glide.load; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.engine.Resource; import java.io.IOException; /** * An interface for decoding resources. * * @param The type the resource will be decoded from (File, InputStream etc). * @param The type of the decoded resource (Bitmap, Drawable etc). */ public interface ResourceDecoder { /** * Returns {@code true} if this decoder is capable of decoding the given source with the given * options, and {@code false} otherwise. * *

    Decoders should make a best effort attempt to quickly determine if they are likely to be * able to decode data, but should not attempt to completely read the given data. A typical * implementation would check the file headers verify they match content the decoder expects to * handle (i.e. a GIF decoder should verify that the image contains the GIF header block. * *

    Decoders that return {@code true} from {@code handles} may still return {@code null} from * {@link #decode(Object, int, int, Options)} if the data is partial or formatted incorrectly. */ boolean handles(@NonNull T source, @NonNull Options options) throws IOException; /** * Returns a decoded resource from the given data or null if no resource could be decoded. * *

    The {@code source} is managed by the caller, there's no need to close it. The returned * {@link Resource} will be {@link Resource#recycle() released} when the engine sees fit. * *

    Note - The {@code width} and {@code height} arguments are hints only, there is no * requirement that the decoded resource exactly match the given dimensions. A typical use case * would be to use the target dimensions to determine how much to downsample Bitmaps by to avoid * overly large allocations. * * @param source The data the resource should be decoded from. * @param width The ideal width in pixels of the decoded resource, or {@link * com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original resource * width. * @param height The ideal height in pixels of the decoded resource, or {@link * com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original resource * height. * @param options A map of string keys to objects that may or may not contain options available to * this particular implementation. Implementations should not assume that any or all of their * option keys are present. However, implementations may assume that if one of their option * keys is present, it's value is non-null and is of the expected type. * @throws IOException typically only if the {@code source} ({@link java.io.InputStream}, {@link * android.os.ParcelFileDescriptor} etc) throws while being read. * @throws OutOfMemoryError is sometimes thrown if the the request produces an overly large result * due to some combination of source size, requested size, source format and requested format. * Callers do/must handle this error and implementations can throw this error. * @throws RuntimeException is thrown by a variety of decoding libraries, including various * Android libraries. Callers do/must handle this error and implementations can throw this * exception or, preferably, more detailed subclasses. */ @Nullable Resource decode(@NonNull T source, int width, int height, @NonNull Options options) throws IOException; } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/ResourceEncoder.java ================================================ package com.bumptech.glide.load; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.Resource; /** * An interface for writing data from a resource to some persistent data store (i.e. a local File * cache). * * @param The type of the data contained by the resource. */ public interface ResourceEncoder extends Encoder> { // specializing the generic arguments @NonNull EncodeStrategy getEncodeStrategy(@NonNull Options options); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/Transformation.java ================================================ package com.bumptech.glide.load; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.Resource; import java.nio.charset.Charset; import java.security.MessageDigest; /** * A class for performing an arbitrary transformation on a resource that implements {@link * #equals(Object)} and {@link #hashCode()}} to identify the transformation in the memory cache and * {@link #updateDiskCacheKey(java.security.MessageDigest)}} to identify the transformation in disk * caches. * *

    Using the fully qualified class name as a static final {@link String} (not {@link * Class#getName()} to avoid proguard obfuscation) is an easy way to implement {@link * #updateDiskCacheKey(java.security.MessageDigest)}} correctly. If additional arguments are * required they can be passed in to the constructor of the {@code Transformation} and then used to * update the {@link java.security.MessageDigest} passed in to {@link * #updateDiskCacheKey(MessageDigest)}. If arguments are primitive types, they can typically easily * be serialized using {@link java.nio.ByteBuffer}. {@link String} types can be serialized with * {@link String#getBytes(Charset)} using the constant {@link #CHARSET}. * *

    Implementations must implement {@link #equals(Object)} and {@link #hashCode()} for * memory caching to work correctly. * * @param The type of the resource being transformed. */ public interface Transformation extends Key { /** * Transforms the given resource and returns the transformed resource. * *

    If the original resource object is not returned, the original resource will be recycled and * it's internal resources may be reused. This means it is not safe to rely on the original * resource or any internal state of the original resource in any new resource that is created. * Usually this shouldn't occur, but if absolutely necessary either the original resource object * can be returned with modified internal state, or the data in the original resource can be * copied into the transformed resource. * *

    If a Transformation is updated, {@link #equals(Object)}, {@link #hashCode()}, and {@link * #updateDiskCacheKey(java.security.MessageDigest)} should all change. If you're using a simple * String key an easy way to do this is to append a version number to your key. Failing to do so * will mean users may see images loaded from cache that had the old version of the Transformation * applied. Changing the return values of those methods will ensure that the cache key has changed * and therefore that any cached resources will be re-generated using the updated Transformation. * *

    During development you may need to either using {@link * com.bumptech.glide.load.engine.DiskCacheStrategy#NONE} or make sure {@link * #updateDiskCacheKey(java.security.MessageDigest)} changes each time you make a change to the * Transformation. Otherwise the resource you request may be loaded from disk cache and your * Transformation may not be called. * * @param context The Application context * @param resource The resource to transform. * @param outWidth The width of the view or target the resource will be displayed in, or {@link * com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original resource * width. * @param outHeight The height of the view or target the resource will be displayed in, or {@link * com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original resource * height. * @return The transformed resource. */ @NonNull Resource transform( @NonNull Context context, @NonNull Resource resource, int outWidth, int outHeight); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/AssetFileDescriptorLocalUriFetcher.java ================================================ package com.bumptech.glide.load.data; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.net.Uri; import androidx.annotation.NonNull; import java.io.FileNotFoundException; import java.io.IOException; /** Fetches an {@link AssetFileDescriptor} for a local {@link android.net.Uri}. */ public final class AssetFileDescriptorLocalUriFetcher extends LocalUriFetcher { public AssetFileDescriptorLocalUriFetcher(ContentResolver contentResolver, Uri uri) { super(contentResolver, uri); } /** * useMediaStoreApisIfAvailable is part of an experiment and the constructor can be removed in a * future version. */ public AssetFileDescriptorLocalUriFetcher( ContentResolver contentResolver, Uri uri, boolean useMediaStoreApisIfAvailable) { super(contentResolver, uri, useMediaStoreApisIfAvailable); } @Override protected AssetFileDescriptor loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { AssetFileDescriptor result = openAssetFileDescriptor(uri); if (result == null) { throw new FileNotFoundException("FileDescriptor is null for: " + uri); } return result; } @Override protected void close(AssetFileDescriptor data) throws IOException { data.close(); } @NonNull @Override public Class getDataClass() { return AssetFileDescriptor.class; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/AssetPathFetcher.java ================================================ package com.bumptech.glide.load.data; import android.content.res.AssetManager; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import java.io.IOException; /** * An abstract class for obtaining data for an asset path using an {@link * android.content.res.AssetManager}. * * @param The type of data obtained from the asset path (InputStream, FileDescriptor etc). */ public abstract class AssetPathFetcher implements DataFetcher { private static final String TAG = "AssetPathFetcher"; private final String assetPath; private final AssetManager assetManager; private T data; // Public API. @SuppressWarnings("WeakerAccess") public AssetPathFetcher(AssetManager assetManager, String assetPath) { this.assetManager = assetManager; this.assetPath = assetPath; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { data = loadResource(assetManager, assetPath); callback.onDataReady(data); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to load data from asset manager", e); } callback.onLoadFailed(e); } } @Override public void cleanup() { if (data == null) { return; } try { close(data); } catch (IOException e) { // Ignored. } } @Override public void cancel() { // Do nothing. } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } /** * Opens the given asset path with the given {@link android.content.res.AssetManager} and returns * the concrete data type returned by the AssetManager. * * @param assetManager An AssetManager to use to open the given path. * @param path A string path pointing to a resource in assets to open. */ protected abstract T loadResource(AssetManager assetManager, String path) throws IOException; /** * Closes the concrete data type if necessary. * * @param data The data to close. */ protected abstract void close(T data) throws IOException; } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/BufferedOutputStream.java ================================================ package com.bumptech.glide.load.data; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.IOException; import java.io.OutputStream; /** * An {@link OutputStream} implementation that recycles and re-uses {@code byte[]}s using the * provided {@link ArrayPool}. */ public final class BufferedOutputStream extends OutputStream { @NonNull private final OutputStream out; private byte[] buffer; private ArrayPool arrayPool; private int index; public BufferedOutputStream(@NonNull OutputStream out, @NonNull ArrayPool arrayPool) { this(out, arrayPool, ArrayPool.STANDARD_BUFFER_SIZE_BYTES); } @VisibleForTesting BufferedOutputStream(@NonNull OutputStream out, ArrayPool arrayPool, int bufferSize) { this.out = out; this.arrayPool = arrayPool; buffer = arrayPool.get(bufferSize, byte[].class); } @Override public void write(int b) throws IOException { buffer[index++] = (byte) b; maybeFlushBuffer(); } @Override public void write(@NonNull byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(@NonNull byte[] b, int initialOffset, int length) throws IOException { int writtenSoFar = 0; do { int remainingToWrite = length - writtenSoFar; int currentOffset = initialOffset + writtenSoFar; // If we still need to write at least the buffer size worth of bytes, we might as well do so // directly and avoid the overhead of copying to the buffer first. if (index == 0 && remainingToWrite >= buffer.length) { out.write(b, currentOffset, remainingToWrite); return; } int remainingSpaceInBuffer = buffer.length - index; int totalBytesToWriteToBuffer = Math.min(remainingToWrite, remainingSpaceInBuffer); System.arraycopy(b, currentOffset, buffer, index, totalBytesToWriteToBuffer); index += totalBytesToWriteToBuffer; writtenSoFar += totalBytesToWriteToBuffer; maybeFlushBuffer(); } while (writtenSoFar < length); } @Override public void flush() throws IOException { flushBuffer(); out.flush(); } private void flushBuffer() throws IOException { if (index > 0) { out.write(buffer, 0, index); index = 0; } } private void maybeFlushBuffer() throws IOException { if (index == buffer.length) { flushBuffer(); } } @Override public void close() throws IOException { try { flush(); } finally { out.close(); } release(); } private void release() { if (buffer != null) { arrayPool.put(buffer); buffer = null; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/DataFetcher.java ================================================ package com.bumptech.glide.load.data; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; /** * Lazily retrieves data that can be used to load a resource. * *

    A new instance is created per resource load by {@link * com.bumptech.glide.load.model.ModelLoader}. {@link #loadData(com.bumptech.glide.Priority, * com.bumptech.glide.load.data.DataFetcher.DataCallback)} may or may not be called for any given * load depending on whether or not the corresponding resource is cached. Cancel also may or may not * be called. If {@link #loadData(com.bumptech.glide.Priority, * com.bumptech.glide.load.data.DataFetcher.DataCallback)}} is called, then so {@link #cleanup()} * will be called. * * @param The type of data to be loaded (InputStream, byte[], File etc). */ public interface DataFetcher { /** * Callback that must be called when data has been loaded and is available, or when the load * fails. * * @param The type of data that will be loaded. */ interface DataCallback { /** * Called with the loaded data if the load succeeded, or with {@code null} if the load failed. */ void onDataReady(@Nullable T data); /** * Called when the load fails. * * @param e a non-null {@link Exception} indicating why the load failed. */ void onLoadFailed(@NonNull Exception e); } /** * Fetch data from which a resource can be decoded. * *

    This will always be called on background thread so it is safe to perform long running tasks * here. Any third party libraries called must be thread safe (or move the work to another thread) * since this method will be called from a thread in a {@link * java.util.concurrent.ExecutorService} that may have more than one background thread. You * MUST use the {@link DataCallback} once the request is complete. * *

    You are free to move the fetch work to another thread and call the callback from there. * *

    This method will only be called when the corresponding resource is not in the cache. * *

    Note - this method will be run on a background thread so blocking I/O is safe. * * @param priority The priority with which the request should be completed. * @param callback The callback to use when the request is complete * @see #cleanup() where the data retuned will be cleaned up */ void loadData(@NonNull Priority priority, @NonNull DataCallback callback); /** * Cleanup or recycle any resources used by this data fetcher. This method will be called in a * finally block after the data provided by {@link #loadData(com.bumptech.glide.Priority, * com.bumptech.glide.load.data.DataFetcher.DataCallback)} has been decoded by the {@link * com.bumptech.glide.load.ResourceDecoder}. * *

    Note - this method will be run on a background thread so blocking I/O is safe. */ void cleanup(); /** * A method that will be called when a load is no longer relevant and has been cancelled. This * method does not need to guarantee that any in process loads do not finish. It also may be * called before a load starts or after it finishes. * *

    The best way to use this method is to cancel any loads that have not yet started, but allow * those that are in process to finish since its we typically will want to display the same * resource in a different view in the near future. * *

    Note - this method will be run on the main thread so it should not perform blocking * operations and should finish quickly. */ void cancel(); /** Returns the class of the data this fetcher will attempt to obtain. */ @NonNull Class getDataClass(); /** Returns the {@link com.bumptech.glide.load.DataSource} this fetcher will return data from. */ @NonNull DataSource getDataSource(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/DataRewinder.java ================================================ package com.bumptech.glide.load.data; import androidx.annotation.NonNull; import java.io.IOException; /** * Responsible for rewinding a stream like data types. * * @param The stream like data type that can be rewound. */ public interface DataRewinder { /** * A factory interface for producing individual {@link * com.bumptech.glide.load.data.DataRewinder}s. * * @param The type of data that the {@link com.bumptech.glide.load.data.DataRewinder} will * wrap. */ interface Factory { /** Returns a new {@link com.bumptech.glide.load.data.DataRewinder} wrapping the given data. */ @NonNull DataRewinder build(@NonNull T data); /** * Returns the class of data this factory can produce {@link * com.bumptech.glide.load.data.DataRewinder}s for. */ @NonNull Class getDataClass(); } /** * Rewinds the wrapped data back to the beginning and returns the re-wound data (or a wrapper for * the re-wound data). * * @return An object pointing to the wrapped data. */ @NonNull T rewindAndGet() throws IOException; /** * Called when this rewinder is no longer needed and can be cleaned up. * *

    The underlying data may still be in use and should not be closed or invalidated. */ void cleanup(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/DataRewinderRegistry.java ================================================ package com.bumptech.glide.load.data; import androidx.annotation.NonNull; import com.bumptech.glide.util.Preconditions; import java.util.HashMap; import java.util.Map; /** * Stores a mapping of data class to {@link com.bumptech.glide.load.data.DataRewinder.Factory} and * allows registration of new types and factories. */ public class DataRewinderRegistry { private final Map, DataRewinder.Factory> rewinders = new HashMap<>(); private static final DataRewinder.Factory DEFAULT_FACTORY = new DataRewinder.Factory() { @NonNull @Override public DataRewinder build(@NonNull Object data) { return new DefaultRewinder(data); } @NonNull @Override public Class getDataClass() { throw new UnsupportedOperationException("Not implemented"); } }; public synchronized void register(@NonNull DataRewinder.Factory factory) { rewinders.put(factory.getDataClass(), factory); } @NonNull @SuppressWarnings("unchecked") public synchronized DataRewinder build(@NonNull T data) { Preconditions.checkNotNull(data); DataRewinder.Factory result = (DataRewinder.Factory) rewinders.get(data.getClass()); if (result == null) { for (DataRewinder.Factory registeredFactory : rewinders.values()) { if (registeredFactory.getDataClass().isAssignableFrom(data.getClass())) { result = (DataRewinder.Factory) registeredFactory; break; } } } if (result == null) { result = (DataRewinder.Factory) DEFAULT_FACTORY; } return result.build(data); } private static final class DefaultRewinder implements DataRewinder { private final Object data; DefaultRewinder(@NonNull Object data) { this.data = data; } @NonNull @Override public Object rewindAndGet() { return data; } @Override public void cleanup() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/ExifOrientationStream.java ================================================ package com.bumptech.glide.load.data; import androidx.annotation.NonNull; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * Adds an exif segment with an orientation attribute to a wrapped {@link InputStream} containing * image data. * *

    This class assumes that the wrapped stream contains an image format that can contain exif * information and performs no verification. */ public final class ExifOrientationStream extends FilterInputStream { /** Allow two bytes for the file format. */ private static final int SEGMENT_START_POSITION = 2; private static final byte[] EXIF_SEGMENT = new byte[] { /* segment start id. */ (byte) 0xFF, /* segment type. */ (byte) 0xE1, /* segmentLength. */ 0x00, (byte) 0x1C, /* exif identifier. */ 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, /* motorola byte order (big endian). */ (byte) 0x4D, (byte) 0x4D, /* filler? */ 0x00, 0x00, /* first id offset. */ 0x00, 0x00, 0x00, 0x08, /* tagCount. */ 0x00, 0x01, /* exif tag type. */ 0x01, 0x12, /* 2 byte format. */ 0x00, 0x02, /* component count. */ 0x00, 0x00, 0x00, 0x01, /* 2 byte orientation value, the first byte of which is always 0. */ 0x00, }; private static final int SEGMENT_LENGTH = EXIF_SEGMENT.length; private static final int ORIENTATION_POSITION = SEGMENT_LENGTH + SEGMENT_START_POSITION; private final byte orientation; private int position; public ExifOrientationStream(InputStream in, int orientation) { super(in); if (orientation < -1 || orientation > 8) { throw new IllegalArgumentException("Cannot add invalid orientation: " + orientation); } this.orientation = (byte) orientation; } @Override public boolean markSupported() { return false; } // No need for synchronized since all we do is throw. @SuppressWarnings("UnsynchronizedOverridesSynchronized") @Override public void mark(int readLimit) { throw new UnsupportedOperationException(); } @Override public int read() throws IOException { final int result; if (position < SEGMENT_START_POSITION || position > ORIENTATION_POSITION) { result = super.read(); } else if (position == ORIENTATION_POSITION) { result = orientation; } else { result = EXIF_SEGMENT[position - SEGMENT_START_POSITION] & 0xFF; } if (result != -1) { position++; } return result; } @Override public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) throws IOException { int read; if (position > ORIENTATION_POSITION) { read = super.read(buffer, byteOffset, byteCount); } else if (position == ORIENTATION_POSITION) { buffer[byteOffset] = orientation; read = 1; } else if (position < SEGMENT_START_POSITION) { read = super.read(buffer, byteOffset, SEGMENT_START_POSITION - position); } else { read = Math.min(ORIENTATION_POSITION - position, byteCount); System.arraycopy(EXIF_SEGMENT, position - SEGMENT_START_POSITION, buffer, byteOffset, read); } if (read > 0) { position += read; } return read; } @Override public long skip(long byteCount) throws IOException { long skipped = super.skip(byteCount); if (skipped > 0) { // See https://errorprone.info/bugpattern/NarrowingCompoundAssignment. position = (int) (position + skipped); } return skipped; } // No need for synchronized since all we do is throw. @SuppressWarnings("UnsynchronizedOverridesSynchronized") @Override public void reset() throws IOException { throw new UnsupportedOperationException(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/FileDescriptorAssetPathFetcher.java ================================================ package com.bumptech.glide.load.data; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import androidx.annotation.NonNull; import java.io.IOException; /** Fetches an {@link android.content.res.AssetFileDescriptor} for an asset path. */ public class FileDescriptorAssetPathFetcher extends AssetPathFetcher { public FileDescriptorAssetPathFetcher(AssetManager assetManager, String assetPath) { super(assetManager, assetPath); } @Override protected AssetFileDescriptor loadResource(AssetManager assetManager, String path) throws IOException { return assetManager.openFd(path); } @Override protected void close(AssetFileDescriptor data) throws IOException { data.close(); } @NonNull @Override public Class getDataClass() { return AssetFileDescriptor.class; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/FileDescriptorLocalUriFetcher.java ================================================ package com.bumptech.glide.load.data; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.ParcelFileDescriptor; import androidx.annotation.NonNull; import java.io.FileNotFoundException; import java.io.IOException; /** Fetches an {@link android.os.ParcelFileDescriptor} for a local {@link android.net.Uri}. */ public class FileDescriptorLocalUriFetcher extends LocalUriFetcher { public FileDescriptorLocalUriFetcher(ContentResolver contentResolver, Uri uri) { super(contentResolver, uri); } /** * useMediaStoreApisIfAvailable is part of an experiment and the constructor can be removed in a * future version. */ public FileDescriptorLocalUriFetcher( ContentResolver contentResolver, Uri uri, boolean useMediaStoreApisIfAvailable) { super(contentResolver, uri, useMediaStoreApisIfAvailable); } @Override protected ParcelFileDescriptor loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { AssetFileDescriptor assetFileDescriptor = openAssetFileDescriptor(uri); if (assetFileDescriptor == null) { throw new FileNotFoundException("FileDescriptor is null for: " + uri); } return assetFileDescriptor.getParcelFileDescriptor(); } @Override protected void close(ParcelFileDescriptor data) throws IOException { data.close(); } @NonNull @Override public Class getDataClass() { return ParcelFileDescriptor.class; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/HttpUrlFetcher.java ================================================ package com.bumptech.glide.load.data; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.HttpException; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.util.ContentLengthInputStream; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Synthetic; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.Map; /** A DataFetcher that retrieves an {@link java.io.InputStream} for a Url. */ public class HttpUrlFetcher implements DataFetcher { private static final String TAG = "HttpUrlFetcher"; private static final int MAXIMUM_REDIRECTS = 5; @VisibleForTesting static final String REDIRECT_HEADER_FIELD = "Location"; @VisibleForTesting static final HttpUrlConnectionFactory DEFAULT_CONNECTION_FACTORY = new DefaultHttpUrlConnectionFactory(); /** Returned when a connection error prevented us from receiving an http error. */ @VisibleForTesting static final int INVALID_STATUS_CODE = -1; private final GlideUrl glideUrl; private final int timeout; private final HttpUrlConnectionFactory connectionFactory; private HttpURLConnection urlConnection; private InputStream stream; private volatile boolean isCancelled; public HttpUrlFetcher(GlideUrl glideUrl, int timeout) { this(glideUrl, timeout, DEFAULT_CONNECTION_FACTORY); } @VisibleForTesting HttpUrlFetcher(GlideUrl glideUrl, int timeout, HttpUrlConnectionFactory connectionFactory) { this.glideUrl = glideUrl; this.timeout = timeout; this.connectionFactory = connectionFactory; } @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { long startTime = LogTime.getLogTime(); try { InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders()); callback.onDataReady(result); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to load data for url", e); } callback.onLoadFailed(e); } finally { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime)); } } } private InputStream loadDataWithRedirects( URL url, int redirects, URL lastUrl, Map headers) throws HttpException { if (redirects >= MAXIMUM_REDIRECTS) { throw new HttpException( "Too many (> " + MAXIMUM_REDIRECTS + ") redirects!", INVALID_STATUS_CODE); } else { // Comparing the URLs using .equals performs additional network I/O and is generally broken. // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html. try { if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) { throw new HttpException("In re-direct loop", INVALID_STATUS_CODE); } } catch (URISyntaxException e) { // Do nothing, this is best effort. } } urlConnection = buildAndConfigureConnection(url, headers); try { // Connect explicitly to avoid errors in decoders if connection fails. urlConnection.connect(); // Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352. stream = urlConnection.getInputStream(); } catch (IOException e) { throw new HttpException( "Failed to connect or obtain data", getHttpStatusCodeOrInvalid(urlConnection), e); } if (isCancelled) { return null; } final int statusCode = getHttpStatusCodeOrInvalid(urlConnection); if (isHttpOk(statusCode)) { return getStreamForSuccessfulRequest(urlConnection); } else if (isHttpRedirect(statusCode)) { String redirectUrlString = urlConnection.getHeaderField(REDIRECT_HEADER_FIELD); if (TextUtils.isEmpty(redirectUrlString)) { throw new HttpException("Received empty or null redirect url", statusCode); } URL redirectUrl; try { redirectUrl = new URL(url, redirectUrlString); } catch (MalformedURLException e) { throw new HttpException("Bad redirect url: " + redirectUrlString, statusCode, e); } // Closing the stream specifically is required to avoid leaking ResponseBodys in addition // to disconnecting the url connection below. See #2352. cleanup(); return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers); } else if (statusCode == INVALID_STATUS_CODE) { throw new HttpException(statusCode); } else { try { throw new HttpException(urlConnection.getResponseMessage(), statusCode); } catch (IOException e) { throw new HttpException("Failed to get a response message", statusCode, e); } } } private static int getHttpStatusCodeOrInvalid(HttpURLConnection urlConnection) { try { return urlConnection.getResponseCode(); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to get a response code", e); } } return INVALID_STATUS_CODE; } private HttpURLConnection buildAndConfigureConnection(URL url, Map headers) throws HttpException { HttpURLConnection urlConnection; try { urlConnection = connectionFactory.build(url); } catch (IOException e) { throw new HttpException("URL.openConnection threw", /* statusCode= */ 0, e); } for (Map.Entry headerEntry : headers.entrySet()) { urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue()); } urlConnection.setConnectTimeout(timeout); urlConnection.setReadTimeout(timeout); urlConnection.setUseCaches(false); urlConnection.setDoInput(true); // Stop the urlConnection instance of HttpUrlConnection from following redirects so that // redirects will be handled by recursive calls to this method, loadDataWithRedirects. urlConnection.setInstanceFollowRedirects(false); return urlConnection; } // Referencing constants is less clear than a simple static method. private static boolean isHttpOk(int statusCode) { return statusCode / 100 == 2; } // Referencing constants is less clear than a simple static method. private static boolean isHttpRedirect(int statusCode) { return statusCode / 100 == 3; } private InputStream getStreamForSuccessfulRequest(HttpURLConnection urlConnection) throws HttpException { try { if (TextUtils.isEmpty(urlConnection.getContentEncoding())) { int contentLength = urlConnection.getContentLength(); stream = ContentLengthInputStream.obtain(urlConnection.getInputStream(), contentLength); } else { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Got non empty content encoding: " + urlConnection.getContentEncoding()); } stream = urlConnection.getInputStream(); } } catch (IOException e) { throw new HttpException( "Failed to obtain InputStream", getHttpStatusCodeOrInvalid(urlConnection), e); } return stream; } @Override public void cleanup() { if (stream != null) { try { stream.close(); } catch (IOException e) { // Ignore } } if (urlConnection != null) { urlConnection.disconnect(); } urlConnection = null; } @Override public void cancel() { // TODO: we should consider disconnecting the url connection here, but we can't do so // directly because cancel is often called on the main thread. isCancelled = true; } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.REMOTE; } interface HttpUrlConnectionFactory { HttpURLConnection build(URL url) throws IOException; } private static class DefaultHttpUrlConnectionFactory implements HttpUrlConnectionFactory { @Synthetic DefaultHttpUrlConnectionFactory() {} @Override public HttpURLConnection build(URL url) throws IOException { return (HttpURLConnection) url.openConnection(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/InputStreamRewinder.java ================================================ package com.bumptech.glide.load.data; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream; import com.bumptech.glide.util.Synthetic; import java.io.IOException; import java.io.InputStream; /** * Implementation for {@link InputStream}s that rewinds streams by wrapping them in a buffered * stream. */ public final class InputStreamRewinder implements DataRewinder { // 5MB. private static final int MARK_READ_LIMIT = 5 * 1024 * 1024; private final RecyclableBufferedInputStream bufferedStream; @Synthetic public InputStreamRewinder(InputStream is, ArrayPool byteArrayPool) { // We don't check is.markSupported() here because RecyclableBufferedInputStream allows resetting // after exceeding MARK_READ_LIMIT, which other InputStreams don't guarantee. bufferedStream = new RecyclableBufferedInputStream(is, byteArrayPool); bufferedStream.mark(MARK_READ_LIMIT); } @NonNull @Override public InputStream rewindAndGet() throws IOException { bufferedStream.reset(); return bufferedStream; } @Override public void cleanup() { bufferedStream.release(); } public void fixMarkLimits() { bufferedStream.fixMarkLimit(); } /** * Factory for producing {@link com.bumptech.glide.load.data.InputStreamRewinder}s from {@link * java.io.InputStream}s. */ public static final class Factory implements DataRewinder.Factory { private final ArrayPool byteArrayPool; public Factory(ArrayPool byteArrayPool) { this.byteArrayPool = byteArrayPool; } @NonNull @Override public DataRewinder build(InputStream data) { return new InputStreamRewinder(data, byteArrayPool); } @NonNull @Override public Class getDataClass() { return InputStream.class; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/LocalUriFetcher.java ================================================ package com.bumptech.glide.load.data; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import java.io.FileNotFoundException; import java.io.IOException; /** * A DataFetcher that uses an {@link android.content.ContentResolver} to load data from a {@link * android.net.Uri} pointing to a local resource. * * @param The type of data that will obtained for the given uri (For example, {@link * java.io.InputStream} or {@link android.os.ParcelFileDescriptor}. */ public abstract class LocalUriFetcher implements DataFetcher { protected final boolean useMediaStoreApisIfAvailable; private static final String TAG = "LocalUriFetcher"; private final Uri uri; private final ContentResolver contentResolver; private T data; /** * Opens an input stream for a uri pointing to a local asset. Only certain uris are supported * * @param contentResolver Any {@link android.content.ContentResolver}. * @param uri A Uri pointing to a local asset. This load will fail if the uri isn't openable by * {@link ContentResolver#openInputStream(android.net.Uri)} * @see ContentResolver#openInputStream(android.net.Uri) */ // Public API. @SuppressWarnings("WeakerAccess") public LocalUriFetcher(ContentResolver contentResolver, Uri uri) { this(contentResolver, uri, /* useMediaStoreApisIfAvailable */ false); } /** * Opens an input stream for a uri pointing to a local asset. Only certain uris are supported * * @param contentResolver Any {@link android.content.ContentResolver}. * @param uri A Uri pointing to a local asset. This load will fail if the uri isn't openable by * {@link ContentResolver#openInputStream(android.net.Uri)} * @param useMediaStoreApisIfAvailable used to decide if the uri should be opened using MediaStore * APIs * @see ContentResolver#openInputStream(android.net.Uri) */ LocalUriFetcher(ContentResolver contentResolver, Uri uri, boolean useMediaStoreApisIfAvailable) { this.contentResolver = contentResolver; this.uri = uri; this.useMediaStoreApisIfAvailable = useMediaStoreApisIfAvailable; } @Override public final void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { try { data = loadResource(uri, contentResolver); callback.onDataReady(data); } catch (FileNotFoundException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to open Uri", e); } callback.onLoadFailed(e); } } @Override public void cleanup() { if (data != null) { try { close(data); } catch (IOException e) { // Ignored. } } } @Override public void cancel() { // Do nothing. } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } /** * Opens an {@link AssetFileDescriptor} for a uri pointing to a local asset. Depending on the * {@code useMediaStoreApisIfAvailable} flag and the availability of MediaStore APIs, the uri may * be opened using MediaStore APIs or {@link * ContentResolver#openAssetFileDescriptor(android.net.Uri, String)}. * * @param uri A Uri pointing to a local asset. */ protected AssetFileDescriptor openAssetFileDescriptor(Uri uri) throws FileNotFoundException { return useMediaStoreApisIfAvailable && MediaStoreUtil.isMediaStoreUri(uri) && MediaStoreUtil.isMediaStoreOpenFileApisAvailable() ? MediaStoreUtil.openAssetFileDescriptor(uri, contentResolver) : contentResolver.openAssetFileDescriptor(uri, "r"); } /** * Returns a concrete data type from the given {@link android.net.Uri} using the given {@link * android.content.ContentResolver}. */ protected abstract T loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException; /** * Closes the concrete data type if necessary. * *

    Note - We can't rely on the closeable interface because it was added after our min API * level. See issue #157. * * @param data The data to close. */ protected abstract void close(T data) throws IOException; } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/ParcelFileDescriptorRewinder.java ================================================ package com.bumptech.glide.load.data; import static android.system.OsConstants.SEEK_SET; import android.os.Build; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.Os; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import java.io.IOException; /** * Implementation for {@link ParcelFileDescriptor}s that rewinds file descriptors by seeking to 0. */ public final class ParcelFileDescriptorRewinder implements DataRewinder { private final InternalRewinder rewinder; public static boolean isSupported() { // Os.lseek() is only supported on API 21+ and does not work in Robolectric. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !"robolectric".equals(Build.FINGERPRINT); } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public ParcelFileDescriptorRewinder(ParcelFileDescriptor parcelFileDescriptor) { rewinder = new InternalRewinder(parcelFileDescriptor); } @NonNull @Override @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public ParcelFileDescriptor rewindAndGet() throws IOException { return rewinder.rewind(); } @Override public void cleanup() { // Do nothing. } /** * Factory for producing {@link ParcelFileDescriptorRewinder}s from {@link ParcelFileDescriptor}s. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public static final class Factory implements DataRewinder.Factory { @NonNull @Override public DataRewinder build( @NonNull ParcelFileDescriptor parcelFileDescriptor) { return new ParcelFileDescriptorRewinder(parcelFileDescriptor); } @NonNull @Override public Class getDataClass() { return ParcelFileDescriptor.class; } } /** * Catching ErrnoException cannot be done in classes that are loaded on APIs < Lollipop. To make * sure that we do not do so, we catch inside this inner class instead of the outer class. The * only reason this class exists is to avoid VerifyError on older APIs. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private static final class InternalRewinder { private final ParcelFileDescriptor parcelFileDescriptor; InternalRewinder(ParcelFileDescriptor parcelFileDescriptor) { this.parcelFileDescriptor = parcelFileDescriptor; } ParcelFileDescriptor rewind() throws IOException { try { Os.lseek(parcelFileDescriptor.getFileDescriptor(), 0, SEEK_SET); } catch (ErrnoException e) { throw new IOException(e); } return parcelFileDescriptor; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/StreamAssetPathFetcher.java ================================================ package com.bumptech.glide.load.data; import android.content.res.AssetManager; import androidx.annotation.NonNull; import java.io.IOException; import java.io.InputStream; /** Fetches an {@link java.io.InputStream} for an asset path. */ public class StreamAssetPathFetcher extends AssetPathFetcher { public StreamAssetPathFetcher(AssetManager assetManager, String assetPath) { super(assetManager, assetPath); } @Override protected InputStream loadResource(AssetManager assetManager, String path) throws IOException { return assetManager.open(path); } @Override protected void close(InputStream data) throws IOException { data.close(); } @NonNull @Override public Class getDataClass() { return InputStream.class; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/StreamLocalUriFetcher.java ================================================ package com.bumptech.glide.load.data; import android.content.ContentResolver; import android.content.UriMatcher; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.Build.VERSION_CODES; import android.provider.ContactsContract; import androidx.annotation.NonNull; import androidx.annotation.RequiresExtension; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; /** Fetches an {@link java.io.InputStream} for a local {@link android.net.Uri}. */ public class StreamLocalUriFetcher extends LocalUriFetcher { /** A lookup uri (e.g. content://com.android.contacts/contacts/lookup/3570i61d948d30808e537) */ private static final int ID_CONTACTS_LOOKUP = 1; /** A contact thumbnail uri (e.g. content://com.android.contacts/contacts/38/photo) */ private static final int ID_CONTACTS_THUMBNAIL = 2; /** A contact uri (e.g. content://com.android.contacts/contacts/38) */ private static final int ID_CONTACTS_CONTACT = 3; /** * A contact display photo (high resolution) uri (e.g. * content://com.android.contacts/5/display_photo) */ private static final int ID_CONTACTS_PHOTO = 4; /** * Uri for optimized search of phones by number (e.g. * content://com.android.contacts/phone_lookup/232323232 */ private static final int ID_LOOKUP_BY_PHONE = 5; /** Match the incoming Uri for special cases which we can handle nicely. */ private static final UriMatcher URI_MATCHER; static { URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); URI_MATCHER.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", ID_CONTACTS_LOOKUP); URI_MATCHER.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", ID_CONTACTS_LOOKUP); URI_MATCHER.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", ID_CONTACTS_THUMBNAIL); URI_MATCHER.addURI(ContactsContract.AUTHORITY, "contacts/#", ID_CONTACTS_CONTACT); URI_MATCHER.addURI(ContactsContract.AUTHORITY, "contacts/#/display_photo", ID_CONTACTS_PHOTO); URI_MATCHER.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", ID_LOOKUP_BY_PHONE); } public StreamLocalUriFetcher(ContentResolver resolver, Uri uri) { super(resolver, uri); } /** * useMediaStoreApisIfAvailable is part of an experiment and the constructor can be removed in a * future version. */ public StreamLocalUriFetcher( ContentResolver resolver, Uri uri, boolean useMediaStoreApisIfAvailable) { super(resolver, uri, useMediaStoreApisIfAvailable); } @Override protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { InputStream inputStream = loadResourceFromUri(uri, contentResolver); if (inputStream == null) { throw new FileNotFoundException("InputStream is null for " + uri); } return inputStream; } private InputStream loadResourceFromUri(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { switch (URI_MATCHER.match(uri)) { case ID_CONTACTS_CONTACT: return openContactPhotoInputStream(contentResolver, uri); case ID_CONTACTS_LOOKUP: case ID_LOOKUP_BY_PHONE: // If it was a Lookup uri then resolve it first, then continue loading the contact uri. uri = ContactsContract.Contacts.lookupContact(contentResolver, uri); if (uri == null) { throw new FileNotFoundException("Contact cannot be found"); } return openContactPhotoInputStream(contentResolver, uri); case ID_CONTACTS_THUMBNAIL: case ID_CONTACTS_PHOTO: case UriMatcher.NO_MATCH: default: if (useMediaStoreApisIfAvailable && MediaStoreUtil.isMediaStoreUri(uri) && MediaStoreUtil.isMediaStoreOpenFileApisAvailable()) { return openMediaStoreFileInputStream(uri, contentResolver); } else { return contentResolver.openInputStream(uri); } } } private InputStream openContactPhotoInputStream(ContentResolver contentResolver, Uri contactUri) { return ContactsContract.Contacts.openContactPhotoInputStream( contentResolver, contactUri, true /*preferHighres*/); } @RequiresExtension( extension = VERSION_CODES.R, version = MediaStoreUtil.MIN_EXTENSION_VERSION_FOR_OPEN_FILE_APIS) private InputStream openMediaStoreFileInputStream(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { AssetFileDescriptor assetFileDescriptor = MediaStoreUtil.openAssetFileDescriptor(uri, contentResolver); if (assetFileDescriptor == null) { throw new FileNotFoundException("FileDescriptor is null for: " + uri); } try { return assetFileDescriptor.createInputStream(); } catch (IOException exception) { try { assetFileDescriptor.close(); } catch (Exception innerException) { // Ignored } throw (FileNotFoundException) new FileNotFoundException("Unable to create stream").initCause(exception); } } @Override protected void close(InputStream data) throws IOException { data.close(); } @NonNull @Override public Class getDataClass() { return InputStream.class; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/mediastore/FileService.java ================================================ package com.bumptech.glide.load.data.mediastore; import java.io.File; class FileService { public boolean exists(File file) { return file.exists(); } public long length(File file) { return file.length(); } public File get(String path) { return new File(path); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/mediastore/MediaStoreUtil.java ================================================ package com.bumptech.glide.load.data.mediastore; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.ext.SdkExtensions; import android.provider.MediaStore; import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.RequiresExtension; import com.bumptech.glide.request.target.Target; import java.io.FileNotFoundException; /** Utility classes for interacting with the media store. */ public final class MediaStoreUtil { public static final int MIN_EXTENSION_VERSION_FOR_OPEN_FILE_APIS = 17; private static final int MINI_THUMB_WIDTH = 512; private static final int MINI_THUMB_HEIGHT = 384; private MediaStoreUtil() { // Utility class. } public static boolean isMediaStoreUri(Uri uri) { return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) && MediaStore.AUTHORITY.equals(uri.getAuthority()); } @ChecksSdkIntAtLeast(api = MIN_EXTENSION_VERSION_FOR_OPEN_FILE_APIS, extension = VERSION_CODES.R) public static boolean isMediaStoreOpenFileApisAvailable() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= MIN_EXTENSION_VERSION_FOR_OPEN_FILE_APIS; } @RequiresExtension( extension = VERSION_CODES.R, version = MIN_EXTENSION_VERSION_FOR_OPEN_FILE_APIS) public static AssetFileDescriptor openAssetFileDescriptor( Uri uri, ContentResolver contentResolver) throws FileNotFoundException { return MediaStore.openAssetFileDescriptor(contentResolver, uri, "r", null); } // Android picker uris contain a "picker" segment: // https://android.googlesource.com/platform/packages/providers/MediaProvider/+/refs/heads/master/src/com/android/providers/media/PickerUriResolver.java#58 public static boolean isAndroidPickerUri(Uri uri) { return isMediaStoreUri(uri) && uri.getPathSegments().contains("picker"); } private static boolean isVideoUri(Uri uri) { return uri.getPathSegments().contains("video"); } public static boolean isMediaStoreVideoUri(Uri uri) { return isMediaStoreUri(uri) && isVideoUri(uri); } public static boolean isMediaStoreImageUri(Uri uri) { return isMediaStoreUri(uri) && !isVideoUri(uri); } public static boolean isThumbnailSize(int width, int height) { return width != Target.SIZE_ORIGINAL && height != Target.SIZE_ORIGINAL && width <= MINI_THUMB_WIDTH && height <= MINI_THUMB_HEIGHT; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/mediastore/ThumbFetcher.java ================================================ package com.bumptech.glide.load.data.mediastore; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.ExifOrientationStream; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; /** * A {@link DataFetcher} implementation for {@link InputStream}s that loads data from thumbnail * files obtained from the {@link MediaStore}. */ @SuppressWarnings("PMD.FieldDeclarationsShouldBeAtStartOfClass") public class ThumbFetcher implements DataFetcher { private static final String TAG = "MediaStoreThumbFetcher"; private final Uri mediaStoreImageUri; private final ThumbnailStreamOpener opener; private InputStream inputStream; public static ThumbFetcher buildImageFetcher(Context context, Uri uri) { return build(context, uri, new ImageThumbnailQuery(context.getContentResolver())); } public static ThumbFetcher buildVideoFetcher(Context context, Uri uri) { return build(context, uri, new VideoThumbnailQuery(context.getContentResolver())); } private static ThumbFetcher build(Context context, Uri uri, ThumbnailQuery query) { ArrayPool byteArrayPool = Glide.get(context).getArrayPool(); ThumbnailStreamOpener opener = new ThumbnailStreamOpener( Glide.get(context).getRegistry().getImageHeaderParsers(), query, byteArrayPool, context.getContentResolver()); return new ThumbFetcher(uri, opener); } @VisibleForTesting ThumbFetcher(Uri mediaStoreImageUri, ThumbnailStreamOpener opener) { this.mediaStoreImageUri = mediaStoreImageUri; this.opener = opener; } @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { try { inputStream = openThumbInputStream(); callback.onDataReady(inputStream); } catch (FileNotFoundException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to find thumbnail file", e); } callback.onLoadFailed(e); } } private InputStream openThumbInputStream() throws FileNotFoundException { InputStream result = opener.open(mediaStoreImageUri); int orientation = -1; if (result != null) { orientation = opener.getOrientation(mediaStoreImageUri); } if (orientation != -1) { result = new ExifOrientationStream(result, orientation); } return result; } @Override public void cleanup() { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { // Ignored. } } } @Override public void cancel() { // Do nothing. } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } static class VideoThumbnailQuery implements ThumbnailQuery { private final ContentResolver contentResolver; VideoThumbnailQuery(ContentResolver contentResolver) { this.contentResolver = contentResolver; } private static final String[] PATH_PROJECTION = {MediaStore.Video.Thumbnails.DATA}; private static final String PATH_SELECTION = MediaStore.Video.Thumbnails.KIND + " = " + MediaStore.Video.Thumbnails.MINI_KIND + " AND " + MediaStore.Video.Thumbnails.VIDEO_ID + " = ?"; @Override public Cursor query(Uri uri) { String videoId = uri.getLastPathSegment(); return contentResolver.query( MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI, PATH_PROJECTION, PATH_SELECTION, new String[] {videoId}, null /*sortOrder*/); } } static class ImageThumbnailQuery implements ThumbnailQuery { private final ContentResolver contentResolver; ImageThumbnailQuery(ContentResolver contentResolver) { this.contentResolver = contentResolver; } private static final String[] PATH_PROJECTION = { MediaStore.Images.Thumbnails.DATA, }; private static final String PATH_SELECTION = MediaStore.Images.Thumbnails.KIND + " = " + MediaStore.Images.Thumbnails.MINI_KIND + " AND " + MediaStore.Images.Thumbnails.IMAGE_ID + " = ?"; @Override public Cursor query(Uri uri) { String imageId = uri.getLastPathSegment(); return contentResolver.query( MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, PATH_PROJECTION, PATH_SELECTION, new String[] {imageId}, null /*sortOrder*/); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/mediastore/ThumbnailQuery.java ================================================ package com.bumptech.glide.load.data.mediastore; import android.database.Cursor; import android.net.Uri; interface ThumbnailQuery { Cursor query(Uri uri); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/data/mediastore/ThumbnailStreamOpener.java ================================================ package com.bumptech.glide.load.data.mediastore; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.List; class ThumbnailStreamOpener { private static final String TAG = "ThumbStreamOpener"; private static final FileService DEFAULT_SERVICE = new FileService(); private final FileService service; private final ThumbnailQuery query; private final ArrayPool byteArrayPool; private final ContentResolver contentResolver; private final List parsers; ThumbnailStreamOpener( List parsers, ThumbnailQuery query, ArrayPool byteArrayPool, ContentResolver contentResolver) { this(parsers, DEFAULT_SERVICE, query, byteArrayPool, contentResolver); } ThumbnailStreamOpener( List parsers, FileService service, ThumbnailQuery query, ArrayPool byteArrayPool, ContentResolver contentResolver) { this.service = service; this.query = query; this.byteArrayPool = byteArrayPool; this.contentResolver = contentResolver; this.parsers = parsers; } int getOrientation(Uri uri) { InputStream is = null; try { is = contentResolver.openInputStream(uri); return ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool); // PMD.AvoidCatchingNPE framework method openInputStream can throw NPEs. } catch (@SuppressWarnings("PMD.AvoidCatchingNPE") IOException | NullPointerException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to open uri: " + uri, e); } } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } } return ImageHeaderParser.UNKNOWN_ORIENTATION; } public InputStream open(Uri uri) throws FileNotFoundException { String path = getPath(uri); if (TextUtils.isEmpty(path)) { return null; } File file = service.get(path); if (!isValid(file)) { return null; } Uri thumbnailUri = Uri.fromFile(file); try { return contentResolver.openInputStream(thumbnailUri); // PMD.AvoidCatchingNPE framework method openInputStream can throw NPEs. } catch ( @SuppressWarnings("PMD.AvoidCatchingNPE") NullPointerException e) { throw (FileNotFoundException) new FileNotFoundException("NPE opening uri: " + uri + " -> " + thumbnailUri).initCause(e); } } @Nullable private String getPath(@NonNull Uri uri) { Cursor cursor = null; try { cursor = query.query(uri); if (cursor != null && cursor.moveToFirst()) { return cursor.getString(0); } else { return null; } } catch (SecurityException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to query for thumbnail for Uri: " + uri, e); } return null; } finally { if (cursor != null) { cursor.close(); } } } private boolean isValid(File file) { return service.exists(file) && 0 < service.length(file); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/ActiveResources.java ================================================ package com.bumptech.glide.load.engine; import android.os.Process; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.EngineResource.ResourceListener; import com.bumptech.glide.util.Executors; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; final class ActiveResources { private final boolean isActiveResourceRetentionAllowed; private final Executor monitorClearedResourcesExecutor; @VisibleForTesting final Map activeEngineResources = new HashMap<>(); private final ReferenceQueue> resourceReferenceQueue = new ReferenceQueue<>(); private ResourceListener listener; private volatile boolean isShutdown; @Nullable private volatile DequeuedResourceCallback cb; ActiveResources(boolean isActiveResourceRetentionAllowed) { this( isActiveResourceRetentionAllowed, java.util.concurrent.Executors.newSingleThreadExecutor( new ThreadFactory() { @Override public Thread newThread(@NonNull final Runnable r) { return new Thread( new Runnable() { @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); r.run(); } }, "glide-active-resources"); } })); } @VisibleForTesting ActiveResources( boolean isActiveResourceRetentionAllowed, Executor monitorClearedResourcesExecutor) { this.isActiveResourceRetentionAllowed = isActiveResourceRetentionAllowed; this.monitorClearedResourcesExecutor = monitorClearedResourcesExecutor; monitorClearedResourcesExecutor.execute( new Runnable() { @Override public void run() { cleanReferenceQueue(); } }); } void setListener(ResourceListener listener) { synchronized (listener) { synchronized (this) { this.listener = listener; } } } synchronized void activate(Key key, EngineResource resource) { ResourceWeakReference toPut = new ResourceWeakReference( key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed); ResourceWeakReference removed = activeEngineResources.put(key, toPut); if (removed != null) { removed.reset(); } } synchronized void deactivate(Key key) { ResourceWeakReference removed = activeEngineResources.remove(key); if (removed != null) { removed.reset(); } } @Nullable synchronized EngineResource get(Key key) { ResourceWeakReference activeRef = activeEngineResources.get(key); if (activeRef == null) { return null; } EngineResource active = activeRef.get(); if (active == null) { cleanupActiveReference(activeRef); } return active; } @SuppressWarnings({"WeakerAccess", "SynchronizeOnNonFinalField"}) @Synthetic void cleanupActiveReference(@NonNull ResourceWeakReference ref) { synchronized (this) { activeEngineResources.remove(ref.key); if (!ref.isCacheable || ref.resource == null) { return; } } EngineResource newResource = new EngineResource<>( ref.resource, /* isMemoryCacheable= */ true, /* isRecyclable= */ false, ref.key, listener); listener.onResourceReleased(ref.key, newResource); } @SuppressWarnings("WeakerAccess") @Synthetic void cleanReferenceQueue() { while (!isShutdown) { try { ResourceWeakReference ref = (ResourceWeakReference) resourceReferenceQueue.remove(); cleanupActiveReference(ref); // This section for testing only. DequeuedResourceCallback current = cb; if (current != null) { current.onResourceDequeued(); } // End for testing only. } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } @VisibleForTesting void setDequeuedResourceCallback(DequeuedResourceCallback cb) { this.cb = cb; } @VisibleForTesting interface DequeuedResourceCallback { void onResourceDequeued(); } @VisibleForTesting void shutdown() { isShutdown = true; if (monitorClearedResourcesExecutor instanceof ExecutorService) { ExecutorService service = (ExecutorService) monitorClearedResourcesExecutor; Executors.shutdownAndAwaitTermination(service); } } @VisibleForTesting static final class ResourceWeakReference extends WeakReference> { @SuppressWarnings("WeakerAccess") @Synthetic final Key key; @SuppressWarnings("WeakerAccess") @Synthetic final boolean isCacheable; @Nullable @SuppressWarnings("WeakerAccess") @Synthetic Resource resource; @Synthetic @SuppressWarnings("WeakerAccess") ResourceWeakReference( @NonNull Key key, @NonNull EngineResource referent, @NonNull ReferenceQueue> queue, boolean isActiveResourceRetentionAllowed) { super(referent, queue); this.key = Preconditions.checkNotNull(key); this.resource = referent.isMemoryCacheable() && isActiveResourceRetentionAllowed ? Preconditions.checkNotNull(referent.getResource()) : null; isCacheable = referent.isMemoryCacheable(); } void reset() { resource = null; clear(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/CallbackException.java ================================================ package com.bumptech.glide.load.engine; /** * An exception indicating that code outside of Glide threw an unexpected exception. * *

    This is useful to allow us to distinguish developer errors on the part of users of Glide from * developer errors on the part of developers of Glide itself. */ final class CallbackException extends RuntimeException { private static final long serialVersionUID = -7530898992688511851L; CallbackException(Throwable cause) { super("Unexpected exception thrown by non-Glide code", cause); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DataCacheGenerator.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoader.LoadData; import com.bumptech.glide.util.pool.GlideTrace; import java.io.File; import java.util.List; /** * Generates {@link com.bumptech.glide.load.data.DataFetcher DataFetchers} from cache files * containing original unmodified source data. */ class DataCacheGenerator implements DataFetcherGenerator, DataFetcher.DataCallback { private final List cacheKeys; private final DecodeHelper helper; private final FetcherReadyCallback cb; private int sourceIdIndex = -1; private Key sourceKey; private List> modelLoaders; private int modelLoaderIndex; private volatile LoadData loadData; // PMD is wrong here, this File must be an instance variable because it may be used across // multiple calls to startNext. @SuppressWarnings("PMD.SingularField") private File cacheFile; DataCacheGenerator(DecodeHelper helper, FetcherReadyCallback cb) { this(helper.getCacheKeys(), helper, cb); } // In some cases we may want to load a specific cache key (when loading from source written to // cache), so we accept a list of keys rather than just obtain the list from the helper. DataCacheGenerator(List cacheKeys, DecodeHelper helper, FetcherReadyCallback cb) { this.cacheKeys = cacheKeys; this.helper = helper; this.cb = cb; } @Override public boolean startNext() { GlideTrace.beginSection("DataCacheGenerator.startNext"); try { while (modelLoaders == null || !hasNextModelLoader()) { sourceIdIndex++; if (sourceIdIndex >= cacheKeys.size()) { return false; } Key sourceId = cacheKeys.get(sourceIdIndex); // PMD.AvoidInstantiatingObjectsInLoops The loop iterates a limited number of times // and the actions it performs are much more expensive than a single allocation. @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") Key originalKey = new DataCacheKey(sourceId, helper.getSignature()); cacheFile = helper.getDiskCache().get(originalKey); if (cacheFile != null) { this.sourceKey = sourceId; modelLoaders = helper.getModelLoaders(cacheFile); modelLoaderIndex = 0; } } loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { ModelLoader modelLoader = modelLoaders.get(modelLoaderIndex++); loadData = modelLoader.buildLoadData( cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; } finally { GlideTrace.endSection(); } } private boolean hasNextModelLoader() { return modelLoaderIndex < modelLoaders.size(); } @Override public void cancel() { LoadData local = loadData; if (local != null) { local.fetcher.cancel(); } } @Override public void onDataReady(Object data) { cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey); } @Override public void onLoadFailed(@NonNull Exception e) { cb.onDataFetcherFailed(sourceKey, e, loadData.fetcher, DataSource.DATA_DISK_CACHE); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DataCacheKey.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import java.security.MessageDigest; /** A cache key for original source data + any requested signature. */ final class DataCacheKey implements Key { private final Key sourceKey; private final Key signature; DataCacheKey(Key sourceKey, Key signature) { this.sourceKey = sourceKey; this.signature = signature; } Key getSourceKey() { return sourceKey; } @Override public boolean equals(Object o) { if (o instanceof DataCacheKey) { DataCacheKey other = (DataCacheKey) o; return sourceKey.equals(other.sourceKey) && signature.equals(other.signature); } return false; } @Override public int hashCode() { int result = sourceKey.hashCode(); result = 31 * result + signature.hashCode(); return result; } @Override public String toString() { return "DataCacheKey{" + "sourceKey=" + sourceKey + ", signature=" + signature + '}'; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { sourceKey.updateDiskCacheKey(messageDigest); signature.updateDiskCacheKey(messageDigest); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DataCacheWriter.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import com.bumptech.glide.load.Encoder; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.cache.DiskCache; import java.io.File; /** * Writes original source data or downsampled/transformed resource data to cache using the provided * {@link com.bumptech.glide.load.Encoder} or {@link com.bumptech.glide.load.ResourceEncoder} and * the given data or {@link com.bumptech.glide.load.engine.Resource}. * * @param The type of data that will be encoded (InputStream, ByteBuffer, * Resource etc). */ class DataCacheWriter implements DiskCache.Writer { private final Encoder encoder; private final DataType data; private final Options options; DataCacheWriter(Encoder encoder, DataType data, Options options) { this.encoder = encoder; this.data = data; this.options = options; } @Override public boolean write(@NonNull File file) { return encoder.encode(data, file, options); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DataFetcherGenerator.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.Nullable; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.data.DataFetcher; /** * Generates a series of {@link com.bumptech.glide.load.data.DataFetcher DataFetchers} using * registered {@link com.bumptech.glide.load.model.ModelLoader ModelLoaders} and a model. */ interface DataFetcherGenerator { /** * Called when the generator has finished loading data from a {@link * com.bumptech.glide.load.data.DataFetcher}. */ interface FetcherReadyCallback { /** Requests that we call startNext() again on a Glide owned thread. */ void reschedule(); /** * Notifies the callback that the load is complete. * * @param sourceKey The id of the loaded data. * @param data The loaded data, or null if the load failed. * @param fetcher The data fetcher we attempted to load from. * @param dataSource The data source we were loading from. * @param attemptedKey The key we were loading data from (may be an alternate). */ void onDataFetcherReady( Key sourceKey, @Nullable Object data, DataFetcher fetcher, DataSource dataSource, Key attemptedKey); /** * Notifies the callback when the load fails. * * @param attemptedKey The key we were using to load (may be an alternate). * @param e The exception that caused the load to fail. * @param fetcher The fetcher we were loading from. * @param dataSource The data source we were loading from. */ void onDataFetcherFailed( Key attemptedKey, Exception e, DataFetcher fetcher, DataSource dataSource); } /** * Attempts to a single new {@link com.bumptech.glide.load.data.DataFetcher} and returns true if a * {@link com.bumptech.glide.load.data.DataFetcher} was started, and false otherwise. */ boolean startNext(); /** * Attempts to cancel the currently running fetcher. * *

    This will be called on the main thread and should complete quickly. */ void cancel(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DecodeHelper.java ================================================ package com.bumptech.glide.load.engine; import com.bumptech.glide.GlideContext; import com.bumptech.glide.Priority; import com.bumptech.glide.Registry; import com.bumptech.glide.load.Encoder; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.load.engine.DecodeJob.DiskCacheProvider; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoader.LoadData; import com.bumptech.glide.load.resource.UnitTransformation; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; final class DecodeHelper { private final List> loadData = new ArrayList<>(); private final List cacheKeys = new ArrayList<>(); private GlideContext glideContext; private Object model; private int width; private int height; private Class resourceClass; private DecodeJob.DiskCacheProvider diskCacheProvider; private Options options; private Map, Transformation> transformations; private Class transcodeClass; private boolean isLoadDataSet; private boolean isCacheKeysSet; private Key signature; private Priority priority; private DiskCacheStrategy diskCacheStrategy; private boolean isTransformationRequired; private boolean isScaleOnlyOrNoTransform; @SuppressWarnings("unchecked") void init( GlideContext glideContext, Object model, Key signature, int width, int height, DiskCacheStrategy diskCacheStrategy, Class resourceClass, Class transcodeClass, Priority priority, Options options, Map, Transformation> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, DiskCacheProvider diskCacheProvider) { this.glideContext = glideContext; this.model = model; this.signature = signature; this.width = width; this.height = height; this.diskCacheStrategy = diskCacheStrategy; this.resourceClass = resourceClass; this.diskCacheProvider = diskCacheProvider; this.transcodeClass = (Class) transcodeClass; this.priority = priority; this.options = options; this.transformations = transformations; this.isTransformationRequired = isTransformationRequired; this.isScaleOnlyOrNoTransform = isScaleOnlyOrNoTransform; } void clear() { glideContext = null; model = null; signature = null; resourceClass = null; transcodeClass = null; options = null; priority = null; transformations = null; diskCacheStrategy = null; loadData.clear(); isLoadDataSet = false; cacheKeys.clear(); isCacheKeysSet = false; } DiskCache getDiskCache() { return diskCacheProvider.getDiskCache(); } DiskCacheStrategy getDiskCacheStrategy() { return diskCacheStrategy; } DataRewinder getRewinder(T data) { return glideContext.getRegistry().getRewinder(data); } Priority getPriority() { return priority; } Options getOptions() { return options; } Key getSignature() { return signature; } int getWidth() { return width; } int getHeight() { return height; } ArrayPool getArrayPool() { return glideContext.getArrayPool(); } Class getTranscodeClass() { return transcodeClass; } Class getModelClass() { return model.getClass(); } List> getRegisteredResourceClasses() { return glideContext .getRegistry() .getRegisteredResourceClasses(model.getClass(), resourceClass, transcodeClass); } boolean hasLoadPath(Class dataClass) { return getLoadPath(dataClass) != null; } LoadPath getLoadPath(Class dataClass) { return glideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass); } boolean isScaleOnlyOrNoTransform() { return isScaleOnlyOrNoTransform; } @SuppressWarnings("unchecked") Transformation getTransformation(Class resourceClass) { Transformation result = (Transformation) transformations.get(resourceClass); if (result == null) { for (Entry, Transformation> entry : transformations.entrySet()) { if (entry.getKey().isAssignableFrom(resourceClass)) { result = (Transformation) entry.getValue(); break; } } } if (result == null) { if (transformations.isEmpty() && isTransformationRequired) { throw new IllegalArgumentException( "Missing transformation for " + resourceClass + ". If you wish to" + " ignore unknown resource types, use the optional transformation methods."); } else { return UnitTransformation.get(); } } return result; } boolean isResourceEncoderAvailable(Resource resource) { return glideContext.getRegistry().isResourceEncoderAvailable(resource); } ResourceEncoder getResultEncoder(Resource resource) { return glideContext.getRegistry().getResultEncoder(resource); } List> getModelLoaders(File file) throws Registry.NoModelLoaderAvailableException { return glideContext.getRegistry().getModelLoaders(file); } boolean isSourceKey(Key key) { List> loadData = getLoadData(); //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = loadData.size(); i < size; i++) { LoadData current = loadData.get(i); if (current.sourceKey.equals(key)) { return true; } } return false; } List> getLoadData() { if (!isLoadDataSet) { isLoadDataSet = true; loadData.clear(); List> modelLoaders = glideContext.getRegistry().getModelLoaders(model); //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = modelLoaders.size(); i < size; i++) { ModelLoader modelLoader = modelLoaders.get(i); LoadData current = modelLoader.buildLoadData(model, width, height, options); if (current != null) { loadData.add(current); } } } return loadData; } List getCacheKeys() { if (!isCacheKeysSet) { isCacheKeysSet = true; cacheKeys.clear(); List> loadData = getLoadData(); //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = loadData.size(); i < size; i++) { LoadData data = loadData.get(i); if (!cacheKeys.contains(data.sourceKey)) { cacheKeys.add(data.sourceKey); } for (int j = 0; j < data.alternateKeys.size(); j++) { if (!cacheKeys.contains(data.alternateKeys.get(j))) { cacheKeys.add(data.alternateKeys.get(j)); } } } } return cacheKeys; } Encoder getSourceEncoder(X data) throws Registry.NoSourceEncoderAvailableException { return glideContext.getRegistry().getSourceEncoder(data); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DecodeJob.java ================================================ package com.bumptech.glide.load.engine; import android.os.Build; import android.os.Process; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pools; import com.bumptech.glide.GlideBuilder.OverrideGlideThreadPriority; import com.bumptech.glide.GlideContext; import com.bumptech.glide.GlideExperiments; import com.bumptech.glide.Priority; import com.bumptech.glide.Registry; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.pool.FactoryPools.Poolable; import com.bumptech.glide.util.pool.GlideTrace; import com.bumptech.glide.util.pool.StateVerifier; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Supplier; /** * A class responsible for decoding resources either from cached data or from the original source * and applying transformations and transcodes. * *

    Note: this class has a natural ordering that is inconsistent with equals. * * @param The type of resource that will be transcoded from the decoded and transformed * resource. */ class DecodeJob implements DataFetcherGenerator.FetcherReadyCallback, Runnable, Comparable>, Poolable { private static final String TAG = "DecodeJob"; /** * {@link com.bumptech.glide.load.Option} to override the OS thread priority of the thread * handling the decode job. * *

    Acceptable values are integer constants defined in {@link android.os.Process}, ranging from * {@link android.os.Process#THREAD_PRIORITY_LOWEST} to (-20). Any exceptions thrown will cause * the override to fail silently and disable overrides on any subsequent jobs. * *

    Must have {@link GlideBuilder#setOverrideGlideThreadPriority(boolean)} experiment enabled to * be used. * *

    This is used for a highly experimental API that may be removed in the future. Please use at * your own risk. */ public static final Option> GLIDE_THREAD_PRIORITY_OVERRIDE = Option.memory("glide_thread_priority_override"); private final DecodeHelper decodeHelper = new DecodeHelper<>(); private final List throwables = new ArrayList<>(); private final StateVerifier stateVerifier = StateVerifier.newInstance(); private final DiskCacheProvider diskCacheProvider; private final Pools.Pool> pool; private final DeferredEncodeManager deferredEncodeManager = new DeferredEncodeManager<>(); private final ReleaseManager releaseManager = new ReleaseManager(); private GlideContext glideContext; private Key signature; private Priority priority; private EngineKey loadKey; private int width; private int height; private DiskCacheStrategy diskCacheStrategy; private Options options; private Callback callback; private int order; private Stage stage; private RunReason runReason; private long startFetchTime; private boolean onlyRetrieveFromCache; private Object model; private GlideExperiments experiments; @Nullable private Supplier glideThreadPriorityOverride; private Thread currentThread; private Key currentSourceKey; private Key currentAttemptingKey; private Object currentData; private DataSource currentDataSource; private DataFetcher currentFetcher; private volatile DataFetcherGenerator currentGenerator; private volatile boolean isCallbackNotified; private volatile boolean isCancelled; private boolean isLoadingFromAlternateCacheKey; DecodeJob(DiskCacheProvider diskCacheProvider, Pools.Pool> pool) { this.diskCacheProvider = diskCacheProvider; this.pool = pool; } DecodeJob init( GlideContext glideContext, Object model, EngineKey loadKey, Key signature, int width, int height, Class resourceClass, Class transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map, Transformation> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, boolean onlyRetrieveFromCache, Options options, Callback callback, int order) { decodeHelper.init( glideContext, model, signature, width, height, diskCacheStrategy, resourceClass, transcodeClass, priority, options, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, diskCacheProvider); this.glideContext = glideContext; this.signature = signature; this.priority = priority; this.loadKey = loadKey; this.width = width; this.height = height; this.diskCacheStrategy = diskCacheStrategy; this.onlyRetrieveFromCache = onlyRetrieveFromCache; this.options = options; this.callback = callback; this.order = order; this.runReason = RunReason.INITIALIZE; this.model = model; this.experiments = glideContext.getExperiments(); this.glideThreadPriorityOverride = options.get(GLIDE_THREAD_PRIORITY_OVERRIDE); return this; } /** * Returns true if this job will attempt to decode a resource from the disk cache, and false if it * will always decode from source. */ boolean willDecodeFromCache() { Stage firstStage = getNextStage(Stage.INITIALIZE); return firstStage == Stage.RESOURCE_CACHE || firstStage == Stage.DATA_CACHE; } /** * Called when this object is no longer in use externally. * * @param isRemovedFromQueue {@code true} if we've been removed from the queue and {@link #run} is * neither in progress nor will ever be called again. */ void release(boolean isRemovedFromQueue) { if (releaseManager.release(isRemovedFromQueue)) { releaseInternal(); } } /** * Called when we've finished encoding (either because the encode process is complete, or because * we don't have anything to encode). */ private void onEncodeComplete() { if (releaseManager.onEncodeComplete()) { releaseInternal(); } } /** Called when the load has failed due to a an error or a series of errors. */ private void onLoadFailed() { if (releaseManager.onFailed()) { releaseInternal(); } } private void releaseInternal() { releaseManager.reset(); deferredEncodeManager.clear(); decodeHelper.clear(); isCallbackNotified = false; glideContext = null; signature = null; options = null; priority = null; loadKey = null; callback = null; stage = null; currentGenerator = null; currentThread = null; currentSourceKey = null; currentData = null; currentDataSource = null; currentFetcher = null; startFetchTime = 0L; isCancelled = false; model = null; throwables.clear(); pool.release(this); } @Override public int compareTo(@NonNull DecodeJob other) { int result = getPriority() - other.getPriority(); if (result == 0) { result = order - other.order; } return result; } private int getPriority() { return priority.ordinal(); } public void cancel() { isCancelled = true; DataFetcherGenerator local = currentGenerator; if (local != null) { local.cancel(); } } // We need to rethrow only CallbackException, but not other types of Throwables. @SuppressWarnings("PMD.AvoidRethrowingException") @Override public void run() { // This should be much more fine grained, but since Java's thread pool implementation silently // swallows all otherwise fatal exceptions, this will at least make it obvious to developers // that something is failing. GlideTrace.beginSectionFormat("DecodeJob#run(reason=%s, model=%s)", runReason, model); // Methods in the try statement can invalidate currentFetcher, so set a local variable here to // ensure that the fetcher is cleaned up either way. DataFetcher localFetcher = currentFetcher; try { if (isCancelled) { notifyFailed(); return; } runWrapped(); } catch (CallbackException e) { // If a callback not controlled by Glide throws an exception, we should avoid the Glide // specific debug logic below. throw e; } catch (Throwable t) { // Catch Throwable and not Exception to handle OOMs. Throwables are swallowed by our // usage of .submit() in GlideExecutor so we're not silently hiding crashes by doing this. We // are however ensuring that our callbacks are always notified when a load fails. Without this // notification, uncaught throwables never notify the corresponding callbacks, which can cause // loads to silently hang forever, a case that's especially bad for users using Futures on // background threads. if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "DecodeJob threw unexpectedly" + ", isCancelled: " + isCancelled + ", stage: " + stage, t); } // When we're encoding we've already notified our callback and it isn't safe to do so again. if (stage != Stage.ENCODE) { throwables.add(t); notifyFailed(); } if (!isCancelled) { throw t; } throw t; } finally { // Keeping track of the fetcher here and calling cleanup is excessively paranoid, we call // close in all cases anyway. if (localFetcher != null) { localFetcher.cleanup(); } GlideTrace.endSection(); } } private void runWrapped() { switch (runReason) { case INITIALIZE: stage = getNextStage(Stage.INITIALIZE); currentGenerator = getNextGenerator(); runGenerators(); break; case SWITCH_TO_SOURCE_SERVICE: runGenerators(); break; case DECODE_DATA: decodeFromRetrievedData(); break; default: throw new IllegalStateException("Unrecognized run reason: " + runReason); } } private DataFetcherGenerator getNextGenerator() { switch (stage) { case RESOURCE_CACHE: return new ResourceCacheGenerator(decodeHelper, this); case DATA_CACHE: return new DataCacheGenerator(decodeHelper, this); case SOURCE: return new SourceGenerator(decodeHelper, this); case FINISHED: return null; default: throw new IllegalStateException("Unrecognized stage: " + stage); } } private void runGenerators() { currentThread = Thread.currentThread(); startFetchTime = LogTime.getLogTime(); boolean isStarted = false; while (!isCancelled && currentGenerator != null && !(isStarted = currentGenerator.startNext())) { stage = getNextStage(stage); currentGenerator = getNextGenerator(); if (stage == Stage.SOURCE) { reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE); return; } } // We've run out of stages and generators, give up. if ((stage == Stage.FINISHED || isCancelled) && !isStarted) { notifyFailed(); } // Otherwise a generator started a new load and we expect to be called back in // onDataFetcherReady. } /** * Restores the OS priority of the Glide thread to the default thread priority of {@link * com.bumptech.glide.load.engine.executor.GlideExecutor}. */ private void restoreThreadPriority() { if (!experiments.isEnabled(OverrideGlideThreadPriority.class)) { throw new IllegalStateException("OverrideGlideThreadPriority experiment is not enabled."); } if (glideThreadPriorityOverride != null && glideThreadPriorityOverride.get() != null) { try { Process.setThreadPriority(Process.myTid(), GlideExecutor.DEFAULT_PRIORITY); } catch (IllegalArgumentException | SecurityException e) { glideThreadPriorityOverride = null; if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Failed to set thread priority; using default priority for any subsequent jobs.", e); } } } } private void notifyFailed() { if (experiments.isEnabled(OverrideGlideThreadPriority.class)) { restoreThreadPriority(); } setNotifiedOrThrow(); GlideException e = new GlideException("Failed to load resource", new ArrayList<>(throwables)); callback.onLoadFailed(e); onLoadFailed(); } private void notifyComplete( Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) { if (experiments.isEnabled(OverrideGlideThreadPriority.class)) { restoreThreadPriority(); } setNotifiedOrThrow(); callback.onResourceReady(resource, dataSource, isLoadedFromAlternateCacheKey); } private void setNotifiedOrThrow() { stateVerifier.throwIfRecycled(); if (isCallbackNotified) { Throwable lastThrown = throwables.isEmpty() ? null : throwables.get(throwables.size() - 1); throw new IllegalStateException("Already notified", lastThrown); } isCallbackNotified = true; } private Stage getNextStage(Stage current) { switch (current) { case INITIALIZE: return diskCacheStrategy.decodeCachedResource() ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE); case RESOURCE_CACHE: return diskCacheStrategy.decodeCachedData() ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE); case DATA_CACHE: // Skip loading from source if the user opted to only retrieve the resource from cache. return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE; case SOURCE: case FINISHED: return Stage.FINISHED; default: throw new IllegalArgumentException("Unrecognized stage: " + current); } } private void reschedule(RunReason runReason) { this.runReason = runReason; callback.reschedule(this); } // This is used by SourceGenerator to ask us to switch back to our thread. Internal methods in // this class should call reschedule with a specific RunReason. @Override public void reschedule() { reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE); } @Override public void onDataFetcherReady( Key sourceKey, Object data, DataFetcher fetcher, DataSource dataSource, Key attemptedKey) { this.currentSourceKey = sourceKey; this.currentData = data; this.currentFetcher = fetcher; this.currentDataSource = dataSource; this.currentAttemptingKey = attemptedKey; this.isLoadingFromAlternateCacheKey = sourceKey != decodeHelper.getCacheKeys().get(0); if (Thread.currentThread() != currentThread) { reschedule(RunReason.DECODE_DATA); } else { GlideTrace.beginSection("DecodeJob.decodeFromRetrievedData"); try { decodeFromRetrievedData(); } finally { GlideTrace.endSection(); } } } @Override public void onDataFetcherFailed( Key attemptedKey, Exception e, DataFetcher fetcher, DataSource dataSource) { fetcher.cleanup(); GlideException exception = new GlideException("Fetching data failed", e); exception.setLoggingDetails(attemptedKey, dataSource, fetcher.getDataClass()); throwables.add(exception); if (Thread.currentThread() != currentThread) { reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE); } else { runGenerators(); } } private void decodeFromRetrievedData() { if (Log.isLoggable(TAG, Log.VERBOSE)) { logWithTimeAndKey( "Retrieved data", startFetchTime, "data: " + currentData + ", cache key: " + currentSourceKey + ", fetcher: " + currentFetcher); } if (experiments.isEnabled(OverrideGlideThreadPriority.class) && glideThreadPriorityOverride != null && glideThreadPriorityOverride.get() != null) { try { Process.setThreadPriority(Process.myTid(), glideThreadPriorityOverride.get().intValue()); } catch (IllegalArgumentException | SecurityException e) { glideThreadPriorityOverride = null; if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Failed to set thread priority; using default priority for any subsequent jobs.", e); } } } Resource resource = null; try { resource = decodeFromData(currentFetcher, currentData, currentDataSource); } catch (GlideException e) { e.setLoggingDetails(currentAttemptingKey, currentDataSource); throwables.add(e); } if (resource != null) { notifyEncodeAndRelease(resource, currentDataSource, isLoadingFromAlternateCacheKey); } else { runGenerators(); } } private void notifyEncodeAndRelease( Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) { GlideTrace.beginSection("DecodeJob.notifyEncodeAndRelease"); try { if (resource instanceof Initializable) { ((Initializable) resource).initialize(); } Resource result = resource; LockedResource lockedResource = null; if (deferredEncodeManager.hasResourceToEncode()) { lockedResource = LockedResource.obtain(resource); result = lockedResource; } notifyComplete(result, dataSource, isLoadedFromAlternateCacheKey); stage = Stage.ENCODE; try { if (deferredEncodeManager.hasResourceToEncode()) { deferredEncodeManager.encode(diskCacheProvider, options); } } finally { if (lockedResource != null) { lockedResource.unlock(); } } // Call onEncodeComplete outside the finally block so that it's not called if the encode // process // throws. onEncodeComplete(); } finally { GlideTrace.endSection(); } } private Resource decodeFromData( DataFetcher fetcher, Data data, DataSource dataSource) throws GlideException { try { if (data == null) { return null; } long startTime = LogTime.getLogTime(); Resource result = decodeFromFetcher(data, dataSource); if (Log.isLoggable(TAG, Log.VERBOSE)) { logWithTimeAndKey("Decoded result " + result, startTime); } return result; } finally { fetcher.cleanup(); } } @SuppressWarnings("unchecked") private Resource decodeFromFetcher(Data data, DataSource dataSource) throws GlideException { LoadPath path = decodeHelper.getLoadPath((Class) data.getClass()); return runLoadPath(data, dataSource, path); } @NonNull private Options getOptionsWithHardwareConfig(DataSource dataSource) { Options options = this.options; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return options; } boolean isHardwareConfigSafe = dataSource == DataSource.RESOURCE_DISK_CACHE || decodeHelper.isScaleOnlyOrNoTransform(); Boolean isHardwareConfigAllowed = options.get(Downsampler.ALLOW_HARDWARE_CONFIG); // If allow hardware config is defined, we can use it if it's set to false or if it's safe to // use the hardware config for the request. if (isHardwareConfigAllowed != null && (!isHardwareConfigAllowed || isHardwareConfigSafe)) { return options; } // If allow hardware config is undefined or is set to true but it's unsafe for us to use the // hardware config for this request, we need to override the config. options = new Options(); options.putAll(this.options); options.set(Downsampler.ALLOW_HARDWARE_CONFIG, isHardwareConfigSafe); return options; } private Resource runLoadPath( Data data, DataSource dataSource, LoadPath path) throws GlideException { Options options = getOptionsWithHardwareConfig(dataSource); DataRewinder rewinder = glideContext.getRegistry().getRewinder(data); try { // ResourceType in DecodeCallback below is required for compilation to work with gradle. return path.load( rewinder, options, width, height, new DecodeCallback(dataSource)); } finally { rewinder.cleanup(); } } private void logWithTimeAndKey(String message, long startTime) { logWithTimeAndKey(message, startTime, null /*extraArgs*/); } private void logWithTimeAndKey(String message, long startTime, String extraArgs) { Log.v( TAG, message + " in " + LogTime.getElapsedMillis(startTime) + ", load key: " + loadKey + (extraArgs != null ? ", " + extraArgs : "") + ", thread: " + Thread.currentThread().getName()); } @NonNull @Override public StateVerifier getVerifier() { return stateVerifier; } @Synthetic @NonNull Resource onResourceDecoded(DataSource dataSource, @NonNull Resource decoded) { @SuppressWarnings("unchecked") Class resourceSubClass = (Class) decoded.get().getClass(); Transformation appliedTransformation = null; Resource transformed = decoded; if (dataSource != DataSource.RESOURCE_DISK_CACHE) { appliedTransformation = decodeHelper.getTransformation(resourceSubClass); transformed = appliedTransformation.transform(glideContext, decoded, width, height); } // TODO: Make this the responsibility of the Transformation. if (!decoded.equals(transformed)) { decoded.recycle(); } final EncodeStrategy encodeStrategy; final ResourceEncoder encoder; if (decodeHelper.isResourceEncoderAvailable(transformed)) { encoder = decodeHelper.getResultEncoder(transformed); encodeStrategy = encoder.getEncodeStrategy(options); } else { encoder = null; encodeStrategy = EncodeStrategy.NONE; } Resource result = transformed; boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey); if (diskCacheStrategy.isResourceCacheable( isFromAlternateCacheKey, dataSource, encodeStrategy)) { if (encoder == null) { throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass()); } final Key key; switch (encodeStrategy) { case SOURCE: key = new DataCacheKey(currentSourceKey, signature); break; case TRANSFORMED: key = new ResourceCacheKey( decodeHelper.getArrayPool(), currentSourceKey, signature, width, height, appliedTransformation, resourceSubClass, options); break; default: throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy); } LockedResource lockedResult = LockedResource.obtain(transformed); deferredEncodeManager.init(key, encoder, lockedResult); result = lockedResult; } return result; } private final class DecodeCallback implements DecodePath.DecodeCallback { private final DataSource dataSource; @Synthetic DecodeCallback(DataSource dataSource) { this.dataSource = dataSource; } @NonNull @Override public Resource onResourceDecoded(@NonNull Resource decoded) { return DecodeJob.this.onResourceDecoded(dataSource, decoded); } } /** * Responsible for indicating when it is safe for the job to be cleared and returned to the pool. */ private static class ReleaseManager { private boolean isReleased; private boolean isEncodeComplete; private boolean isFailed; @Synthetic ReleaseManager() {} synchronized boolean release(boolean isRemovedFromQueue) { isReleased = true; return isComplete(isRemovedFromQueue); } synchronized boolean onEncodeComplete() { isEncodeComplete = true; return isComplete(false /*isRemovedFromQueue*/); } synchronized boolean onFailed() { isFailed = true; return isComplete(false /*isRemovedFromQueue*/); } synchronized void reset() { isEncodeComplete = false; isReleased = false; isFailed = false; } private boolean isComplete(boolean isRemovedFromQueue) { return (isFailed || isRemovedFromQueue || isEncodeComplete) && isReleased; } } /** * Allows transformed resources to be encoded after the transcoded result is already delivered to * requestors. */ private static class DeferredEncodeManager { private Key key; private ResourceEncoder encoder; private LockedResource toEncode; @Synthetic DeferredEncodeManager() {} // We just need the encoder and resource type to match, which this will enforce. @SuppressWarnings("unchecked") void init(Key key, ResourceEncoder encoder, LockedResource toEncode) { this.key = key; this.encoder = (ResourceEncoder) encoder; this.toEncode = (LockedResource) toEncode; } void encode(DiskCacheProvider diskCacheProvider, Options options) { GlideTrace.beginSection("DecodeJob.encode"); try { diskCacheProvider .getDiskCache() .put(key, new DataCacheWriter<>(encoder, toEncode, options)); } finally { toEncode.unlock(); GlideTrace.endSection(); } } boolean hasResourceToEncode() { return toEncode != null; } void clear() { key = null; encoder = null; toEncode = null; } } interface Callback { void onResourceReady( Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey); void onLoadFailed(GlideException e); void reschedule(DecodeJob job); } interface DiskCacheProvider { DiskCache getDiskCache(); } /** Why we're being executed again. */ private enum RunReason { /** The first time we've been submitted. */ INITIALIZE, /** We want to switch from the disk cache service to the source executor. */ SWITCH_TO_SOURCE_SERVICE, /** * We retrieved some data on a thread we don't own and want to switch back to our thread to * process the data. */ DECODE_DATA, } /** Where we're trying to decode data from. */ private enum Stage { /** The initial stage. */ INITIALIZE, /** Decode from a cached resource. */ RESOURCE_CACHE, /** Decode from cached source data. */ DATA_CACHE, /** Decode from retrieved source. */ SOURCE, /** Encoding transformed resources after a successful load. */ ENCODE, /** No more viable stages. */ FINISHED, } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DecodePath.java ================================================ package com.bumptech.glide.load.engine; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.util.Pools.Pool; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Attempts to decode and transcode resource type from a given data type. * * @param The type of data ResourceType that will be decoded from. * @param The type of intermediate resource that will be decoded. * @param The final type of resource that will be transcoded from ResourceType and * returned to the caller. */ public class DecodePath { private static final String TAG = "DecodePath"; private final Class dataClass; private final List> decoders; private final ResourceTranscoder transcoder; private final Pool> listPool; private final String failureMessage; public DecodePath( Class dataClass, Class resourceClass, Class transcodeClass, List> decoders, ResourceTranscoder transcoder, Pool> listPool) { this.dataClass = dataClass; this.decoders = decoders; this.transcoder = transcoder; this.listPool = listPool; failureMessage = "Failed DecodePath{" + dataClass.getSimpleName() + "->" + resourceClass.getSimpleName() + "->" + transcodeClass.getSimpleName() + "}"; } public Resource decode( DataRewinder rewinder, int width, int height, @NonNull Options options, DecodeCallback callback) throws GlideException { Resource decoded = decodeResource(rewinder, width, height, options); Resource transformed = callback.onResourceDecoded(decoded); return transcoder.transcode(transformed, options); } @NonNull private Resource decodeResource( DataRewinder rewinder, int width, int height, @NonNull Options options) throws GlideException { List exceptions = Preconditions.checkNotNull(listPool.acquire()); try { return decodeResourceWithList(rewinder, width, height, options, exceptions); } finally { listPool.release(exceptions); } } @NonNull private Resource decodeResourceWithList( DataRewinder rewinder, int width, int height, @NonNull Options options, List exceptions) throws GlideException { Resource result = null; //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = decoders.size(); i < size; i++) { ResourceDecoder decoder = decoders.get(i); try { DataType data = rewinder.rewindAndGet(); if (decoder.handles(data, options)) { data = rewinder.rewindAndGet(); result = decoder.decode(data, width, height, options); } // Some decoders throw unexpectedly. If they do, we shouldn't fail the entire load path, but // instead log and continue. See #2406 for an example. } catch (IOException | RuntimeException | OutOfMemoryError e) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Failed to decode data for " + decoder, e); } exceptions.add(e); } if (result != null) { break; } } if (result == null) { throw new GlideException(failureMessage, new ArrayList<>(exceptions)); } return result; } @Override public String toString() { return "DecodePath{" + " dataClass=" + dataClass + ", decoders=" + decoders + ", transcoder=" + transcoder + '}'; } interface DecodeCallback { @NonNull Resource onResourceDecoded(@NonNull Resource resource); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/DiskCacheStrategy.java ================================================ package com.bumptech.glide.load.engine; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.EncodeStrategy; /** Set of available caching strategies for media. */ public abstract class DiskCacheStrategy { /** * Caches remote data with both {@link #DATA} and {@link #RESOURCE}, and local data with {@link * #RESOURCE} only. */ public static final DiskCacheStrategy ALL = new DiskCacheStrategy() { @Override public boolean isDataCacheable(DataSource dataSource) { return dataSource == DataSource.REMOTE; } @Override public boolean isResourceCacheable( boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) { return dataSource != DataSource.RESOURCE_DISK_CACHE && dataSource != DataSource.MEMORY_CACHE; } @Override public boolean decodeCachedResource() { return true; } @Override public boolean decodeCachedData() { return true; } }; /** Saves no data to cache. */ public static final DiskCacheStrategy NONE = new DiskCacheStrategy() { @Override public boolean isDataCacheable(DataSource dataSource) { return false; } @Override public boolean isResourceCacheable( boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) { return false; } @Override public boolean decodeCachedResource() { return false; } @Override public boolean decodeCachedData() { return false; } }; /** Writes retrieved data directly to the disk cache before it's decoded. */ public static final DiskCacheStrategy DATA = new DiskCacheStrategy() { @Override public boolean isDataCacheable(DataSource dataSource) { return dataSource != DataSource.DATA_DISK_CACHE && dataSource != DataSource.MEMORY_CACHE; } @Override public boolean isResourceCacheable( boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) { return false; } @Override public boolean decodeCachedResource() { return false; } @Override public boolean decodeCachedData() { return true; } }; /** Writes resources to disk after they've been decoded. */ public static final DiskCacheStrategy RESOURCE = new DiskCacheStrategy() { @Override public boolean isDataCacheable(DataSource dataSource) { return false; } @Override public boolean isResourceCacheable( boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) { return dataSource != DataSource.RESOURCE_DISK_CACHE && dataSource != DataSource.MEMORY_CACHE; } @Override public boolean decodeCachedResource() { return true; } @Override public boolean decodeCachedData() { return false; } }; /** * Tries to intelligently choose a strategy based on the data source of the {@link * com.bumptech.glide.load.data.DataFetcher} and the {@link * com.bumptech.glide.load.EncodeStrategy} of the {@link com.bumptech.glide.load.ResourceEncoder} * (if an {@link com.bumptech.glide.load.ResourceEncoder} is available). */ public static final DiskCacheStrategy AUTOMATIC = new DiskCacheStrategy() { @Override public boolean isDataCacheable(DataSource dataSource) { return dataSource == DataSource.REMOTE; } @SuppressWarnings("checkstyle:UnnecessaryParentheses") // Readability @Override public boolean isResourceCacheable( boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) { return ((isFromAlternateCacheKey && dataSource == DataSource.DATA_DISK_CACHE) || dataSource == DataSource.LOCAL) && encodeStrategy == EncodeStrategy.TRANSFORMED; } @Override public boolean decodeCachedResource() { return true; } @Override public boolean decodeCachedData() { return true; } }; /** * Returns true if this request should cache the original unmodified data. * * @param dataSource Indicates where the data was originally retrieved. */ public abstract boolean isDataCacheable(DataSource dataSource); /** * Returns true if this request should cache the final transformed resource. * * @param isFromAlternateCacheKey {@code true} if the resource we've decoded was loaded using an * alternative, rather than the primary, cache key. * @param dataSource Indicates where the data used to decode the resource was originally * retrieved. * @param encodeStrategy The {@link EncodeStrategy} the {@link * com.bumptech.glide.load.ResourceEncoder} will use to encode the resource. */ public abstract boolean isResourceCacheable( boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy); /** Returns true if this request should attempt to decode cached resource data. */ public abstract boolean decodeCachedResource(); /** Returns true if this request should attempt to decode cached source data. */ public abstract boolean decodeCachedData(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/Engine.java ================================================ package com.bumptech.glide.load.engine; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.util.Pools; import com.bumptech.glide.GlideContext; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.EngineResource.ResourceListener; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.DiskCacheAdapter; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.request.ResourceCallback; import com.bumptech.glide.util.Executors; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.pool.FactoryPools; import java.util.Map; import java.util.concurrent.Executor; /** Responsible for starting loads and managing active and cached resources. */ public class Engine implements EngineJobListener, MemoryCache.ResourceRemovedListener, EngineResource.ResourceListener { private static final String TAG = "Engine"; private static final int JOB_POOL_SIZE = 150; private static final boolean VERBOSE_IS_LOGGABLE = Log.isLoggable(TAG, Log.VERBOSE); private final Jobs jobs; private final EngineKeyFactory keyFactory; private final MemoryCache cache; private final EngineJobFactory engineJobFactory; private final ResourceRecycler resourceRecycler; private final LazyDiskCacheProvider diskCacheProvider; private final DecodeJobFactory decodeJobFactory; private final ActiveResources activeResources; public Engine( MemoryCache memoryCache, DiskCache.Factory diskCacheFactory, GlideExecutor diskCacheExecutor, GlideExecutor sourceExecutor, GlideExecutor sourceUnlimitedExecutor, GlideExecutor animationExecutor, boolean isActiveResourceRetentionAllowed) { this( memoryCache, diskCacheFactory, diskCacheExecutor, sourceExecutor, sourceUnlimitedExecutor, animationExecutor, /* jobs= */ null, /* keyFactory= */ null, /* activeResources= */ null, /* engineJobFactory= */ null, /* decodeJobFactory= */ null, /* resourceRecycler= */ null, isActiveResourceRetentionAllowed); } @VisibleForTesting Engine( MemoryCache cache, DiskCache.Factory diskCacheFactory, GlideExecutor diskCacheExecutor, GlideExecutor sourceExecutor, GlideExecutor sourceUnlimitedExecutor, GlideExecutor animationExecutor, Jobs jobs, EngineKeyFactory keyFactory, ActiveResources activeResources, EngineJobFactory engineJobFactory, DecodeJobFactory decodeJobFactory, ResourceRecycler resourceRecycler, boolean isActiveResourceRetentionAllowed) { this.cache = cache; this.diskCacheProvider = new LazyDiskCacheProvider(diskCacheFactory); if (activeResources == null) { activeResources = new ActiveResources(isActiveResourceRetentionAllowed); } this.activeResources = activeResources; activeResources.setListener(this); if (keyFactory == null) { keyFactory = new EngineKeyFactory(); } this.keyFactory = keyFactory; if (jobs == null) { jobs = new Jobs(); } this.jobs = jobs; if (engineJobFactory == null) { engineJobFactory = new EngineJobFactory( diskCacheExecutor, sourceExecutor, sourceUnlimitedExecutor, animationExecutor, /* engineJobListener= */ this, /* resourceListener= */ this); } this.engineJobFactory = engineJobFactory; if (decodeJobFactory == null) { decodeJobFactory = new DecodeJobFactory(diskCacheProvider); } this.decodeJobFactory = decodeJobFactory; if (resourceRecycler == null) { resourceRecycler = new ResourceRecycler(); } this.resourceRecycler = resourceRecycler; cache.setResourceRemovedListener(this); } /** * Starts a load for the given arguments. * *

    Must be called on the main thread. * *

    The flow for any request is as follows: * *

      *
    • Check the current set of actively used resources, return the active resource if present, * and move any newly inactive resources into the memory cache. *
    • Check the memory cache and provide the cached resource if present. *
    • Check the current set of in progress loads and add the cb to the in progress load if one * is present. *
    • Start a new load. *
    * *

    Active resources are those that have been provided to at least one request and have not yet * been released. Once all consumers of a resource have released that resource, the resource then * goes to cache. If the resource is ever returned to a new consumer from cache, it is re-added to * the active resources. If the resource is evicted from the cache, its resources are recycled and * re-used if possible and the resource is discarded. There is no strict requirement that * consumers release their resources so active resources are held weakly. * * @param width The target width in pixels of the desired resource. * @param height The target height in pixels of the desired resource. * @param cb The callback that will be called when the load completes. */ public LoadStatus load( GlideContext glideContext, Object model, Key signature, int width, int height, Class resourceClass, Class transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map, Transformation> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor) { long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0; EngineKey key = keyFactory.buildKey( model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource memoryResource; synchronized (this) { memoryResource = loadFromMemory(key, isMemoryCacheable, startTime); if (memoryResource == null) { return waitForExistingOrStartNewJob( glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime); } } // Avoid calling back while holding the engine lock, doing so makes it easier for callers to // deadlock. cb.onResourceReady( memoryResource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false); return null; } private LoadStatus waitForExistingOrStartNewJob( GlideContext glideContext, Object model, Key signature, int width, int height, Class resourceClass, Class transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map, Transformation> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor, EngineKey key, long startTime) { EngineJob current = jobs.get(key, onlyRetrieveFromCache); if (current != null) { current.addCallback(cb, callbackExecutor); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Added to existing load", startTime, key); } return new LoadStatus(cb, current); } EngineJob engineJob = engineJobFactory.build( key, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache); DecodeJob decodeJob = decodeJobFactory.build( glideContext, model, key, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, onlyRetrieveFromCache, options, engineJob); jobs.put(key, engineJob); engineJob.addCallback(cb, callbackExecutor); engineJob.start(decodeJob); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Started new load", startTime, key); } return new LoadStatus(cb, engineJob); } @Nullable private EngineResource loadFromMemory( EngineKey key, boolean isMemoryCacheable, long startTime) { if (!isMemoryCacheable) { return null; } EngineResource active = loadFromActiveResources(key); if (active != null) { if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Loaded resource from active resources", startTime, key); } return active; } EngineResource cached = loadFromCache(key); if (cached != null) { if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Loaded resource from cache", startTime, key); } return cached; } return null; } private static void logWithTimeAndKey(String log, long startTime, Key key) { Log.v(TAG, log + " in " + LogTime.getElapsedMillis(startTime) + "ms, key: " + key); } @Nullable private EngineResource loadFromActiveResources(Key key) { EngineResource active = activeResources.get(key); if (active != null) { active.acquire(); } return active; } private EngineResource loadFromCache(Key key) { EngineResource cached = getEngineResourceFromCache(key); if (cached != null) { cached.acquire(); activeResources.activate(key, cached); } return cached; } private EngineResource getEngineResourceFromCache(Key key) { Resource cached = cache.remove(key); final EngineResource result; if (cached == null) { result = null; } else if (cached instanceof EngineResource) { // Save an object allocation if we've cached an EngineResource (the typical case). result = (EngineResource) cached; } else { result = new EngineResource<>( cached, /* isMemoryCacheable= */ true, /* isRecyclable= */ true, key, /* listener= */ this); } return result; } public void release(Resource resource) { if (resource instanceof EngineResource) { ((EngineResource) resource).release(); } else { throw new IllegalArgumentException("Cannot release anything but an EngineResource"); } } @SuppressWarnings("unchecked") @Override public synchronized void onEngineJobComplete( EngineJob engineJob, Key key, EngineResource resource) { // A null resource indicates that the load failed, usually due to an exception. if (resource != null && resource.isMemoryCacheable()) { activeResources.activate(key, resource); } jobs.removeIfCurrent(key, engineJob); } @Override public synchronized void onEngineJobCancelled(EngineJob engineJob, Key key) { jobs.removeIfCurrent(key, engineJob); } @Override public void onResourceRemoved(@NonNull final Resource resource) { // Avoid deadlock with RequestManagers when recycling triggers recursive clear() calls. // See b/145519760. resourceRecycler.recycle(resource, /* forceNextFrame= */ true); } @Override public void onResourceReleased(Key cacheKey, EngineResource resource) { activeResources.deactivate(cacheKey); if (resource.isMemoryCacheable()) { cache.put(cacheKey, resource); } else { resourceRecycler.recycle(resource, /* forceNextFrame= */ false); } } public void clearDiskCache() { diskCacheProvider.getDiskCache().clear(); } @VisibleForTesting public void shutdown() { engineJobFactory.shutdown(); diskCacheProvider.clearDiskCacheIfCreated(); activeResources.shutdown(); } /** * Allows a request to indicate it no longer is interested in a given load. * *

    Non-final for mocking. */ public class LoadStatus { private final EngineJob engineJob; private final ResourceCallback cb; LoadStatus(ResourceCallback cb, EngineJob engineJob) { this.cb = cb; this.engineJob = engineJob; } public void cancel() { // Acquire the Engine lock so that a new request can't get access to a particular EngineJob // just after the EngineJob has been cancelled. Without this lock, we'd allow new requests // to find the cancelling EngineJob in our Jobs data structure. With this lock, the EngineJob // is both cancelled and removed from Jobs atomically. synchronized (Engine.this) { engineJob.removeCallback(cb); } } } private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider { private final DiskCache.Factory factory; private volatile DiskCache diskCache; LazyDiskCacheProvider(DiskCache.Factory factory) { this.factory = factory; } @VisibleForTesting synchronized void clearDiskCacheIfCreated() { if (diskCache == null) { return; } diskCache.clear(); } @Override public DiskCache getDiskCache() { if (diskCache == null) { synchronized (this) { if (diskCache == null) { diskCache = factory.build(); } if (diskCache == null) { diskCache = new DiskCacheAdapter(); } } } return diskCache; } } @VisibleForTesting static class DecodeJobFactory { @Synthetic final DecodeJob.DiskCacheProvider diskCacheProvider; @Synthetic final Pools.Pool> pool = FactoryPools.threadSafe( JOB_POOL_SIZE, new FactoryPools.Factory>() { @Override public DecodeJob create() { return new DecodeJob<>(diskCacheProvider, pool); } }); private int creationOrder; DecodeJobFactory(DecodeJob.DiskCacheProvider diskCacheProvider) { this.diskCacheProvider = diskCacheProvider; } @SuppressWarnings("unchecked") DecodeJob build( GlideContext glideContext, Object model, EngineKey loadKey, Key signature, int width, int height, Class resourceClass, Class transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map, Transformation> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, boolean onlyRetrieveFromCache, Options options, DecodeJob.Callback callback) { DecodeJob result = Preconditions.checkNotNull((DecodeJob) pool.acquire()); return result.init( glideContext, model, loadKey, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, onlyRetrieveFromCache, options, callback, creationOrder++); } } @VisibleForTesting static class EngineJobFactory { @Synthetic final GlideExecutor diskCacheExecutor; @Synthetic final GlideExecutor sourceExecutor; @Synthetic final GlideExecutor sourceUnlimitedExecutor; @Synthetic final GlideExecutor animationExecutor; @Synthetic final EngineJobListener engineJobListener; @Synthetic final ResourceListener resourceListener; @Synthetic final Pools.Pool> pool = FactoryPools.threadSafe( JOB_POOL_SIZE, new FactoryPools.Factory>() { @Override public EngineJob create() { return new EngineJob<>( diskCacheExecutor, sourceExecutor, sourceUnlimitedExecutor, animationExecutor, engineJobListener, resourceListener, pool); } }); EngineJobFactory( GlideExecutor diskCacheExecutor, GlideExecutor sourceExecutor, GlideExecutor sourceUnlimitedExecutor, GlideExecutor animationExecutor, EngineJobListener engineJobListener, ResourceListener resourceListener) { this.diskCacheExecutor = diskCacheExecutor; this.sourceExecutor = sourceExecutor; this.sourceUnlimitedExecutor = sourceUnlimitedExecutor; this.animationExecutor = animationExecutor; this.engineJobListener = engineJobListener; this.resourceListener = resourceListener; } @VisibleForTesting void shutdown() { Executors.shutdownAndAwaitTermination(diskCacheExecutor); Executors.shutdownAndAwaitTermination(sourceExecutor); Executors.shutdownAndAwaitTermination(sourceUnlimitedExecutor); Executors.shutdownAndAwaitTermination(animationExecutor); } @SuppressWarnings("unchecked") EngineJob build( Key key, boolean isMemoryCacheable, boolean useUnlimitedSourceGeneratorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache) { EngineJob result = Preconditions.checkNotNull((EngineJob) pool.acquire()); return result.init( key, isMemoryCacheable, useUnlimitedSourceGeneratorPool, useAnimationPool, onlyRetrieveFromCache); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/EngineJob.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.util.Pools; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.EngineResource.ResourceListener; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.request.ResourceCallback; import com.bumptech.glide.util.Executors; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.pool.FactoryPools.Poolable; import com.bumptech.glide.util.pool.StateVerifier; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** * A class that manages a load by adding and removing callbacks for for the load and notifying * callbacks when the load completes. */ class EngineJob implements DecodeJob.Callback, Poolable { private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory(); @SuppressWarnings("WeakerAccess") @Synthetic final ResourceCallbacksAndExecutors cbs = new ResourceCallbacksAndExecutors(); private final StateVerifier stateVerifier = StateVerifier.newInstance(); private final ResourceListener resourceListener; private final Pools.Pool> pool; private final EngineResourceFactory engineResourceFactory; private final EngineJobListener engineJobListener; private final GlideExecutor diskCacheExecutor; private final GlideExecutor sourceExecutor; private final GlideExecutor sourceUnlimitedExecutor; private final GlideExecutor animationExecutor; private final AtomicInteger pendingCallbacks = new AtomicInteger(); private Key key; private boolean isCacheable; private boolean useUnlimitedSourceGeneratorPool; private boolean useAnimationPool; private boolean onlyRetrieveFromCache; private Resource resource; @SuppressWarnings("WeakerAccess") @Synthetic DataSource dataSource; private boolean hasResource; @SuppressWarnings("WeakerAccess") @Synthetic GlideException exception; private boolean hasLoadFailed; @SuppressWarnings("WeakerAccess") @Synthetic EngineResource engineResource; private DecodeJob decodeJob; // Checked primarily on the main thread, but also on other threads in reschedule. private volatile boolean isCancelled; private boolean isLoadedFromAlternateCacheKey; EngineJob( GlideExecutor diskCacheExecutor, GlideExecutor sourceExecutor, GlideExecutor sourceUnlimitedExecutor, GlideExecutor animationExecutor, EngineJobListener engineJobListener, ResourceListener resourceListener, Pools.Pool> pool) { this( diskCacheExecutor, sourceExecutor, sourceUnlimitedExecutor, animationExecutor, engineJobListener, resourceListener, pool, DEFAULT_FACTORY); } @VisibleForTesting EngineJob( GlideExecutor diskCacheExecutor, GlideExecutor sourceExecutor, GlideExecutor sourceUnlimitedExecutor, GlideExecutor animationExecutor, EngineJobListener engineJobListener, ResourceListener resourceListener, Pools.Pool> pool, EngineResourceFactory engineResourceFactory) { this.diskCacheExecutor = diskCacheExecutor; this.sourceExecutor = sourceExecutor; this.sourceUnlimitedExecutor = sourceUnlimitedExecutor; this.animationExecutor = animationExecutor; this.engineJobListener = engineJobListener; this.resourceListener = resourceListener; this.pool = pool; this.engineResourceFactory = engineResourceFactory; } @VisibleForTesting synchronized EngineJob init( Key key, boolean isCacheable, boolean useUnlimitedSourceGeneratorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache) { this.key = key; this.isCacheable = isCacheable; this.useUnlimitedSourceGeneratorPool = useUnlimitedSourceGeneratorPool; this.useAnimationPool = useAnimationPool; this.onlyRetrieveFromCache = onlyRetrieveFromCache; return this; } public synchronized void start(DecodeJob decodeJob) { this.decodeJob = decodeJob; GlideExecutor executor = decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor(); executor.execute(decodeJob); } synchronized void addCallback(final ResourceCallback cb, Executor callbackExecutor) { stateVerifier.throwIfRecycled(); cbs.add(cb, callbackExecutor); if (hasResource) { // Acquire early so that the resource isn't recycled while the Runnable below is still sitting // in the executors queue. incrementPendingCallbacks(1); callbackExecutor.execute(new CallResourceReady(cb)); } else if (hasLoadFailed) { incrementPendingCallbacks(1); callbackExecutor.execute(new CallLoadFailed(cb)); } else { Preconditions.checkArgument(!isCancelled, "Cannot add callbacks to a cancelled EngineJob"); } } @SuppressWarnings("WeakerAccess") @Synthetic @GuardedBy("this") void callCallbackOnResourceReady(ResourceCallback cb) { try { // This is overly broad, some Glide code is actually called here, but it's much // simpler to encapsulate here than to do so at the actual call point in the // Request implementation. cb.onResourceReady(engineResource, dataSource, isLoadedFromAlternateCacheKey); } catch (Throwable t) { throw new CallbackException(t); } } @SuppressWarnings("WeakerAccess") @Synthetic @GuardedBy("this") void callCallbackOnLoadFailed(ResourceCallback cb) { // This is overly broad, some Glide code is actually called here, but it's much // simpler to encapsulate here than to do so at the actual call point in the Request // implementation. try { cb.onLoadFailed(exception); } catch (Throwable t) { throw new CallbackException(t); } } synchronized void removeCallback(ResourceCallback cb) { stateVerifier.throwIfRecycled(); cbs.remove(cb); if (cbs.isEmpty()) { cancel(); boolean isFinishedRunning = hasResource || hasLoadFailed; if (isFinishedRunning && pendingCallbacks.get() == 0) { release(); } } } boolean onlyRetrieveFromCache() { return onlyRetrieveFromCache; } private GlideExecutor getActiveSourceExecutor() { return useUnlimitedSourceGeneratorPool ? sourceUnlimitedExecutor : (useAnimationPool ? animationExecutor : sourceExecutor); } // Exposed for testing. void cancel() { if (isDone()) { return; } isCancelled = true; decodeJob.cancel(); engineJobListener.onEngineJobCancelled(this, key); } // Exposed for testing. synchronized boolean isCancelled() { return isCancelled; } private boolean isDone() { return hasLoadFailed || hasResource || isCancelled; } // We have to post Runnables in a loop. Typically there will be very few callbacks. AccessorMethod // seems to be a false positive @SuppressWarnings({ "WeakerAccess", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AccessorMethodGeneration" }) @Synthetic void notifyCallbacksOfResult() { ResourceCallbacksAndExecutors copy; Key localKey; EngineResource localResource; synchronized (this) { stateVerifier.throwIfRecycled(); if (isCancelled) { // TODO: Seems like we might as well put this in the memory cache instead of just recycling // it since we've gotten this far... resource.recycle(); release(); return; } else if (cbs.isEmpty()) { throw new IllegalStateException("Received a resource without any callbacks to notify"); } else if (hasResource) { throw new IllegalStateException("Already have resource"); } engineResource = engineResourceFactory.build(resource, isCacheable, key, resourceListener); // Hold on to resource for duration of our callbacks below so we don't recycle it in the // middle of notifying if it synchronously released by one of the callbacks. Acquire it under // a lock here so that any newly added callback that executes before the next locked section // below can't recycle the resource before we call the callbacks. hasResource = true; copy = cbs.copy(); incrementPendingCallbacks(copy.size() + 1); localKey = key; localResource = engineResource; } engineJobListener.onEngineJobComplete(this, localKey, localResource); for (final ResourceCallbackAndExecutor entry : copy) { entry.executor.execute(new CallResourceReady(entry.cb)); } decrementPendingCallbacks(); } @SuppressWarnings("WeakerAccess") @Synthetic synchronized void incrementPendingCallbacks(int count) { Preconditions.checkArgument(isDone(), "Not yet complete!"); if (pendingCallbacks.getAndAdd(count) == 0 && engineResource != null) { engineResource.acquire(); } } @SuppressWarnings("WeakerAccess") @Synthetic void decrementPendingCallbacks() { EngineResource toRelease = null; synchronized (this) { stateVerifier.throwIfRecycled(); Preconditions.checkArgument(isDone(), "Not yet complete!"); int decremented = pendingCallbacks.decrementAndGet(); Preconditions.checkArgument(decremented >= 0, "Can't decrement below 0"); if (decremented == 0) { toRelease = engineResource; release(); } } if (toRelease != null) { toRelease.release(); } } private synchronized void release() { if (key == null) { throw new IllegalArgumentException(); } cbs.clear(); key = null; engineResource = null; resource = null; hasLoadFailed = false; isCancelled = false; hasResource = false; isLoadedFromAlternateCacheKey = false; decodeJob.release(/* isRemovedFromQueue= */ false); decodeJob = null; exception = null; dataSource = null; pool.release(this); } @Override public void onResourceReady( Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) { synchronized (this) { this.resource = resource; this.dataSource = dataSource; this.isLoadedFromAlternateCacheKey = isLoadedFromAlternateCacheKey; } notifyCallbacksOfResult(); } @Override public void onLoadFailed(GlideException e) { synchronized (this) { this.exception = e; } notifyCallbacksOfException(); } @Override public void reschedule(DecodeJob job) { // Even if the job is cancelled here, it still needs to be scheduled so that it can clean itself // up. getActiveSourceExecutor().execute(job); } // We have to post Runnables in a loop. Typically there will be very few callbacks. Acessor method // warning seems to be false positive. @SuppressWarnings({ "WeakerAccess", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AccessorMethodGeneration" }) @Synthetic void notifyCallbacksOfException() { ResourceCallbacksAndExecutors copy; Key localKey; synchronized (this) { stateVerifier.throwIfRecycled(); if (isCancelled) { release(); return; } else if (cbs.isEmpty()) { throw new IllegalStateException("Received an exception without any callbacks to notify"); } else if (hasLoadFailed) { throw new IllegalStateException("Already failed once"); } hasLoadFailed = true; localKey = key; copy = cbs.copy(); // One for each callback below, plus one for ourselves so that we finish if a callback runs on // another thread before we finish scheduling all of them. incrementPendingCallbacks(copy.size() + 1); } engineJobListener.onEngineJobComplete(this, localKey, /* resource= */ null); for (ResourceCallbackAndExecutor entry : copy) { entry.executor.execute(new CallLoadFailed(entry.cb)); } decrementPendingCallbacks(); } @NonNull @Override public StateVerifier getVerifier() { return stateVerifier; } private class CallLoadFailed implements Runnable { private final ResourceCallback cb; CallLoadFailed(ResourceCallback cb) { this.cb = cb; } @Override public void run() { // Make sure we always acquire the request lock, then the EngineJob lock to avoid deadlock // (b/136032534). synchronized (cb.getLock()) { synchronized (EngineJob.this) { if (cbs.contains(cb)) { callCallbackOnLoadFailed(cb); } decrementPendingCallbacks(); } } } } private class CallResourceReady implements Runnable { private final ResourceCallback cb; CallResourceReady(ResourceCallback cb) { this.cb = cb; } @Override public void run() { // Make sure we always acquire the request lock, then the EngineJob lock to avoid deadlock // (b/136032534). synchronized (cb.getLock()) { synchronized (EngineJob.this) { if (cbs.contains(cb)) { // Acquire for this particular callback. engineResource.acquire(); callCallbackOnResourceReady(cb); removeCallback(cb); } decrementPendingCallbacks(); } } } } static final class ResourceCallbacksAndExecutors implements Iterable { private final List callbacksAndExecutors; ResourceCallbacksAndExecutors() { this(new ArrayList(2)); } ResourceCallbacksAndExecutors(List callbacksAndExecutors) { this.callbacksAndExecutors = callbacksAndExecutors; } void add(ResourceCallback cb, Executor executor) { callbacksAndExecutors.add(new ResourceCallbackAndExecutor(cb, executor)); } void remove(ResourceCallback cb) { callbacksAndExecutors.remove(defaultCallbackAndExecutor(cb)); } boolean contains(ResourceCallback cb) { return callbacksAndExecutors.contains(defaultCallbackAndExecutor(cb)); } boolean isEmpty() { return callbacksAndExecutors.isEmpty(); } int size() { return callbacksAndExecutors.size(); } void clear() { callbacksAndExecutors.clear(); } ResourceCallbacksAndExecutors copy() { return new ResourceCallbacksAndExecutors(new ArrayList<>(callbacksAndExecutors)); } private static ResourceCallbackAndExecutor defaultCallbackAndExecutor(ResourceCallback cb) { return new ResourceCallbackAndExecutor(cb, Executors.directExecutor()); } @NonNull @Override public Iterator iterator() { return callbacksAndExecutors.iterator(); } } static final class ResourceCallbackAndExecutor { final ResourceCallback cb; final Executor executor; ResourceCallbackAndExecutor(ResourceCallback cb, Executor executor) { this.cb = cb; this.executor = executor; } @Override public boolean equals(Object o) { if (o instanceof ResourceCallbackAndExecutor) { ResourceCallbackAndExecutor other = (ResourceCallbackAndExecutor) o; return cb.equals(other.cb); } return false; } @Override public int hashCode() { return cb.hashCode(); } } @VisibleForTesting static class EngineResourceFactory { public EngineResource build( Resource resource, boolean isMemoryCacheable, Key key, ResourceListener listener) { return new EngineResource<>( resource, isMemoryCacheable, /* isRecyclable= */ true, key, listener); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/EngineJobListener.java ================================================ package com.bumptech.glide.load.engine; import com.bumptech.glide.load.Key; interface EngineJobListener { void onEngineJobComplete(EngineJob engineJob, Key key, EngineResource resource); void onEngineJobCancelled(EngineJob engineJob, Key key); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/EngineKey.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.util.Preconditions; import java.security.MessageDigest; import java.util.Map; /** An in memory only cache key used to multiplex loads. */ class EngineKey implements Key { private final Object model; private final int width; private final int height; private final Class resourceClass; private final Class transcodeClass; private final Key signature; private final Map, Transformation> transformations; private final Options options; private int hashCode; EngineKey( Object model, Key signature, int width, int height, Map, Transformation> transformations, Class resourceClass, Class transcodeClass, Options options) { this.model = Preconditions.checkNotNull(model); this.signature = Preconditions.checkNotNull(signature, "Signature must not be null"); this.width = width; this.height = height; this.transformations = Preconditions.checkNotNull(transformations); this.resourceClass = Preconditions.checkNotNull(resourceClass, "Resource class must not be null"); this.transcodeClass = Preconditions.checkNotNull(transcodeClass, "Transcode class must not be null"); this.options = Preconditions.checkNotNull(options); } @Override public boolean equals(Object o) { if (o instanceof EngineKey) { EngineKey other = (EngineKey) o; return model.equals(other.model) && signature.equals(other.signature) && height == other.height && width == other.width && transformations.equals(other.transformations) && resourceClass.equals(other.resourceClass) && transcodeClass.equals(other.transcodeClass) && options.equals(other.options); } return false; } @Override public int hashCode() { if (hashCode == 0) { hashCode = model.hashCode(); hashCode = 31 * hashCode + signature.hashCode(); hashCode = 31 * hashCode + width; hashCode = 31 * hashCode + height; hashCode = 31 * hashCode + transformations.hashCode(); hashCode = 31 * hashCode + resourceClass.hashCode(); hashCode = 31 * hashCode + transcodeClass.hashCode(); hashCode = 31 * hashCode + options.hashCode(); } return hashCode; } @Override public String toString() { return "EngineKey{" + "model=" + model + ", width=" + width + ", height=" + height + ", resourceClass=" + resourceClass + ", transcodeClass=" + transcodeClass + ", signature=" + signature + ", hashCode=" + hashCode + ", transformations=" + transformations + ", options=" + options + '}'; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { throw new UnsupportedOperationException(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/EngineKeyFactory.java ================================================ package com.bumptech.glide.load.engine; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import java.util.Map; class EngineKeyFactory { @SuppressWarnings("rawtypes") EngineKey buildKey( Object model, Key signature, int width, int height, Map, Transformation> transformations, Class resourceClass, Class transcodeClass, Options options) { return new EngineKey( model, signature, width, height, transformations, resourceClass, transcodeClass, options); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/EngineResource.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.util.Preconditions; /** * A wrapper resource that allows reference counting a wrapped {@link * com.bumptech.glide.load.engine.Resource} interface. * * @param The type of data returned by the wrapped {@link Resource}. */ class EngineResource implements Resource { private final boolean isMemoryCacheable; private final boolean isRecyclable; private final Resource resource; private final ResourceListener listener; private final Key key; private int acquired; private boolean isRecycled; interface ResourceListener { void onResourceReleased(Key key, EngineResource resource); } EngineResource( Resource toWrap, boolean isMemoryCacheable, boolean isRecyclable, Key key, ResourceListener listener) { resource = Preconditions.checkNotNull(toWrap); this.isMemoryCacheable = isMemoryCacheable; this.isRecyclable = isRecyclable; this.key = key; this.listener = Preconditions.checkNotNull(listener); } Resource getResource() { return resource; } boolean isMemoryCacheable() { return isMemoryCacheable; } @NonNull @Override public Class getResourceClass() { return resource.getResourceClass(); } @NonNull @Override public Z get() { return resource.get(); } @Override public int getSize() { return resource.getSize(); } @Override public synchronized void recycle() { if (acquired > 0) { throw new IllegalStateException("Cannot recycle a resource while it is still acquired"); } if (isRecycled) { throw new IllegalStateException("Cannot recycle a resource that has already been recycled"); } isRecycled = true; if (isRecyclable) { resource.recycle(); } } /** * Increments the number of consumers using the wrapped resource. Must be called on the main * thread. * *

    This must be called with a number corresponding to the number of new consumers each time new * consumers begin using the wrapped resource. It is always safer to call acquire more often than * necessary. Generally external users should never call this method, the framework will take care * of this for you. */ synchronized void acquire() { if (isRecycled) { throw new IllegalStateException("Cannot acquire a recycled resource"); } ++acquired; } /** * Decrements the number of consumers using the wrapped resource. Must be called on the main * thread. * *

    This must only be called when a consumer that called the {@link #acquire()} method is now * done with the resource. Generally external users should never call this method, the framework * will take care of this for you. */ // listener is effectively final. @SuppressWarnings("SynchronizeOnNonFinalField") void release() { boolean release = false; synchronized (this) { if (acquired <= 0) { throw new IllegalStateException("Cannot release a recycled or not yet acquired resource"); } if (--acquired == 0) { release = true; } } if (release) { listener.onResourceReleased(key, this); } } @Override public synchronized String toString() { return "EngineResource{" + "isMemoryCacheable=" + isMemoryCacheable + ", listener=" + listener + ", key=" + key + ", acquired=" + acquired + ", isRecycled=" + isRecycled + ", resource=" + resource + '}'; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/GlideException.java ================================================ package com.bumptech.glide.load.engine; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** An exception with zero or more causes indicating why a load in Glide failed. */ // Public API. @SuppressWarnings("WeakerAccess") public final class GlideException extends Exception { private static final long serialVersionUID = 1L; private static final StackTraceElement[] EMPTY_ELEMENTS = new StackTraceElement[0]; private final List causes; private Key key; private DataSource dataSource; private Class dataClass; private String detailMessage; @Nullable private Exception exception; public GlideException(String message) { this(message, Collections.emptyList()); } public GlideException(String detailMessage, Throwable cause) { this(detailMessage, Collections.singletonList(cause)); } public GlideException(String detailMessage, List causes) { this.detailMessage = detailMessage; setStackTrace(EMPTY_ELEMENTS); this.causes = causes; } void setLoggingDetails(Key key, DataSource dataSource) { setLoggingDetails(key, dataSource, null); } void setLoggingDetails(Key key, DataSource dataSource, Class dataClass) { this.key = key; this.dataSource = dataSource; this.dataClass = dataClass; } /** * Sets a stack trace that includes where the request originated. * *

    This is an experimental API that may be removed in the future. */ public void setOrigin(@Nullable Exception exception) { this.exception = exception; } /** * Returns an {@link Exception} with a stack trace that includes where the request originated (if * previously set via {@link #setOrigin(Exception)}) * *

    This is an experimental API that may be removed in the future. */ @Nullable public Exception getOrigin() { return exception; } // No need to synchronize when doing nothing whatsoever. @SuppressWarnings("UnsynchronizedOverridesSynchronized") @Override public Throwable fillInStackTrace() { // Avoid an expensive allocation by doing nothing here. Causes should contain all relevant // stack traces. return this; } /** * Returns a list of causes that are immediate children of this exception. * *

    Causes may or may not be {@link GlideException GlideExceptions}. Causes may also not be root * causes, and in turn my have been caused by other failures. * * @see #getRootCauses() */ public List getCauses() { return causes; } /** * Returns the list of root causes that are the leaf nodes of all children of this exception. * *

    Use this method to do things like look for http exceptions that indicate the load may have * failed due to an error that can be retried. Keep in mind that because Glide may attempt to load * a given model using multiple different pathways, there may be multiple related or unrelated * reasons for a load to fail. */ public List getRootCauses() { List rootCauses = new ArrayList<>(); addRootCauses(this, rootCauses); return rootCauses; } /** * Logs all root causes using the given tag. * *

    Each root cause is logged separately to avoid throttling. {@link #printStackTrace()} will * provide a more succinct overview of why the exception occurred, although it does not include * complete stack traces. */ public void logRootCauses(String tag) { List causes = getRootCauses(); for (int i = 0, size = causes.size(); i < size; i++) { Log.i(tag, "Root cause (" + (i + 1) + " of " + size + ")", causes.get(i)); } } private void addRootCauses(Throwable throwable, List rootCauses) { if (throwable instanceof GlideException) { GlideException glideException = (GlideException) throwable; for (Throwable t : glideException.getCauses()) { addRootCauses(t, rootCauses); } } else if (throwable != null) { rootCauses.add(throwable); } } @Override public void printStackTrace() { printStackTrace(System.err); } @Override public void printStackTrace(PrintStream err) { printStackTrace((Appendable) err); } @Override public void printStackTrace(PrintWriter err) { printStackTrace((Appendable) err); } private void printStackTrace(Appendable appendable) { appendExceptionMessage(this, appendable); appendCauses(getCauses(), new IndentedAppendable(appendable)); } // PMD doesn't seem to notice that we're allocating the builder with the suggested size. @SuppressWarnings("PMD.InsufficientStringBufferDeclaration") @Override public String getMessage() { StringBuilder result = new StringBuilder(71) .append(detailMessage) .append(dataClass != null ? ", " + dataClass : "") .append(dataSource != null ? ", " + dataSource : "") .append(key != null ? ", " + key : ""); List rootCauses = getRootCauses(); if (rootCauses.isEmpty()) { return result.toString(); } else if (rootCauses.size() == 1) { result.append("\nThere was 1 root cause:"); } else { result.append("\nThere were ").append(rootCauses.size()).append(" root causes:"); } for (Throwable cause : rootCauses) { result .append('\n') .append(cause.getClass().getName()) .append('(') .append(cause.getMessage()) .append(')'); } result.append("\n call GlideException#logRootCauses(String) for more detail"); return result.toString(); } // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should // never happen. @SuppressWarnings("PMD.PreserveStackTrace") private static void appendExceptionMessage(Throwable t, Appendable appendable) { try { appendable.append(t.getClass().toString()).append(": ").append(t.getMessage()).append('\n'); } catch (IOException e1) { throw new RuntimeException(t); } } // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should // never happen. @SuppressWarnings("PMD.PreserveStackTrace") private static void appendCauses(List causes, Appendable appendable) { try { appendCausesWrapped(causes, appendable); } catch (IOException e) { throw new RuntimeException(e); } } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") private static void appendCausesWrapped(List causes, Appendable appendable) throws IOException { int size = causes.size(); for (int i = 0; i < size; i++) { appendable .append("Cause (") .append(String.valueOf(i + 1)) .append(" of ") .append(String.valueOf(size)) .append("): "); Throwable cause = causes.get(i); if (cause instanceof GlideException) { GlideException glideCause = (GlideException) cause; glideCause.printStackTrace(appendable); } else { appendExceptionMessage(cause, appendable); } } } private static final class IndentedAppendable implements Appendable { private static final String EMPTY_SEQUENCE = ""; private static final String INDENT = " "; private final Appendable appendable; private boolean printedNewLine = true; IndentedAppendable(Appendable appendable) { this.appendable = appendable; } @Override public Appendable append(char c) throws IOException { if (printedNewLine) { printedNewLine = false; appendable.append(INDENT); } printedNewLine = c == '\n'; appendable.append(c); return this; } @Override public Appendable append(@Nullable CharSequence charSequence) throws IOException { charSequence = safeSequence(charSequence); return append(charSequence, 0, charSequence.length()); } @Override public Appendable append(@Nullable CharSequence charSequence, int start, int end) throws IOException { charSequence = safeSequence(charSequence); if (printedNewLine) { printedNewLine = false; appendable.append(INDENT); } printedNewLine = charSequence.length() > 0 && charSequence.charAt(end - 1) == '\n'; appendable.append(charSequence, start, end); return this; } @NonNull private CharSequence safeSequence(@Nullable CharSequence sequence) { if (sequence == null) { return EMPTY_SEQUENCE; } else { return sequence; } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/Initializable.java ================================================ package com.bumptech.glide.load.engine; /** * A callback allowing a resource to do some optimization on a background thread before being * returned to the ui. */ public interface Initializable { /** Called on a background thread so the {@link Resource} can do some eager initialization. */ void initialize(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/Jobs.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.Key; import java.util.Collections; import java.util.HashMap; import java.util.Map; final class Jobs { private final Map> jobs = new HashMap<>(); private final Map> onlyCacheJobs = new HashMap<>(); @VisibleForTesting Map> getAll() { return Collections.unmodifiableMap(jobs); } EngineJob get(Key key, boolean onlyRetrieveFromCache) { return getJobMap(onlyRetrieveFromCache).get(key); } void put(Key key, EngineJob job) { getJobMap(job.onlyRetrieveFromCache()).put(key, job); } void removeIfCurrent(Key key, EngineJob expected) { Map> jobMap = getJobMap(expected.onlyRetrieveFromCache()); if (expected.equals(jobMap.get(key))) { jobMap.remove(key); } } private Map> getJobMap(boolean onlyRetrieveFromCache) { return onlyRetrieveFromCache ? onlyCacheJobs : jobs; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/LoadPath.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import androidx.core.util.Pools.Pool; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.util.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * For a given {@link com.bumptech.glide.load.data.DataFetcher} for a given data class, attempts to * fetch the data and then run it through one or more {@link * com.bumptech.glide.load.engine.DecodePath}s. * * @param The type of data that will be fetched. * @param The type of intermediate resource that will be decoded within one of the * {@link com.bumptech.glide.load.engine.DecodePath}s. * @param The type of resource that will be returned as the result if the load and one * of the decode paths succeeds. */ public class LoadPath { private final Class dataClass; private final Pool> listPool; private final List> decodePaths; private final String failureMessage; public LoadPath( Class dataClass, Class resourceClass, Class transcodeClass, List> decodePaths, Pool> listPool) { this.dataClass = dataClass; this.listPool = listPool; this.decodePaths = Preconditions.checkNotEmpty(decodePaths); failureMessage = "Failed LoadPath{" + dataClass.getSimpleName() + "->" + resourceClass.getSimpleName() + "->" + transcodeClass.getSimpleName() + "}"; } public Resource load( DataRewinder rewinder, @NonNull Options options, int width, int height, DecodePath.DecodeCallback decodeCallback) throws GlideException { List throwables = Preconditions.checkNotNull(listPool.acquire()); try { return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables); } finally { listPool.release(throwables); } } private Resource loadWithExceptionList( DataRewinder rewinder, @NonNull Options options, int width, int height, DecodePath.DecodeCallback decodeCallback, List exceptions) throws GlideException { Resource result = null; //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = decodePaths.size(); i < size; i++) { DecodePath path = decodePaths.get(i); try { result = path.decode(rewinder, width, height, options, decodeCallback); } catch (GlideException e) { exceptions.add(e); } if (result != null) { break; } } if (result == null) { throw new GlideException(failureMessage, new ArrayList<>(exceptions)); } return result; } public Class getDataClass() { return dataClass; } @Override public String toString() { return "LoadPath{" + "decodePaths=" + Arrays.toString(decodePaths.toArray()) + '}'; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/LockedResource.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import androidx.core.util.Pools; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.pool.FactoryPools; import com.bumptech.glide.util.pool.StateVerifier; /** * A resource that defers any calls to {@link Resource#recycle()} until after {@link #unlock()} is * called. * *

    If the resource was recycled prior to {@link #unlock()}, then {@link #unlock()} will also * recycle the resource. */ final class LockedResource implements Resource, FactoryPools.Poolable { private static final Pools.Pool> POOL = FactoryPools.threadSafe( 20, new FactoryPools.Factory>() { @Override public LockedResource create() { return new LockedResource(); } }); private final StateVerifier stateVerifier = StateVerifier.newInstance(); private Resource toWrap; private boolean isLocked; private boolean isRecycled; @SuppressWarnings("unchecked") @NonNull static LockedResource obtain(Resource resource) { LockedResource result = Preconditions.checkNotNull((LockedResource) POOL.acquire()); result.init(resource); return result; } @SuppressWarnings("WeakerAccess") @Synthetic LockedResource() {} private void init(Resource toWrap) { isRecycled = false; isLocked = true; this.toWrap = toWrap; } private void release() { toWrap = null; POOL.release(this); } synchronized void unlock() { stateVerifier.throwIfRecycled(); if (!isLocked) { throw new IllegalStateException("Already unlocked"); } this.isLocked = false; if (isRecycled) { recycle(); } } @NonNull @Override public Class getResourceClass() { return toWrap.getResourceClass(); } @NonNull @Override public Z get() { return toWrap.get(); } @Override public int getSize() { return toWrap.getSize(); } @Override public synchronized void recycle() { stateVerifier.throwIfRecycled(); this.isRecycled = true; if (!isLocked) { toWrap.recycle(); release(); } } @NonNull @Override public StateVerifier getVerifier() { return stateVerifier; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/Resource.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; /** * A resource interface that wraps a particular type so that it can be pooled and reused. * * @param The type of resource wrapped by this class. */ public interface Resource { /** Returns the {@link Class} of the wrapped resource. */ @NonNull Class getResourceClass(); /** * Returns an instance of the wrapped resource. * *

    Note - This does not have to be the same instance of the wrapped resource class and in fact * it is often appropriate to return a new instance for each call. For example, {@link * android.graphics.drawable.Drawable Drawable}s should only be used by a single {@link * android.view.View View} at a time so each call to this method for Resources that wrap {@link * android.graphics.drawable.Drawable Drawable}s should always return a new {@link * android.graphics.drawable.Drawable Drawable}. */ @NonNull Z get(); /** * Returns the size in bytes of the wrapped resource to use to determine how much of the memory * cache this resource uses. */ int getSize(); /** * Cleans up and recycles internal resources. * *

    It is only safe to call this method if there are no current resource consumers and if this * method has not yet been called. Typically this occurs at one of two times: * *

      *
    • During a resource load when the resource is transformed or transcoded before any consumer * have ever had access to this resource *
    • After all consumers have released this resource and it has been evicted from the cache *
    * * For most users of this class, the only time this method should ever be called is during * transformations or transcoders, the framework will call this method when all consumers have * released this resource and it has been evicted from the cache. */ void recycle(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/ResourceCacheGenerator.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoader.LoadData; import com.bumptech.glide.util.pool.GlideTrace; import java.io.File; import java.util.List; /** * Generates {@link com.bumptech.glide.load.data.DataFetcher DataFetchers} from cache files * containing downsampled/transformed resource data. */ class ResourceCacheGenerator implements DataFetcherGenerator, DataFetcher.DataCallback { private final FetcherReadyCallback cb; private final DecodeHelper helper; private int sourceIdIndex; private int resourceClassIndex = -1; private Key sourceKey; private List> modelLoaders; private int modelLoaderIndex; private volatile LoadData loadData; // PMD is wrong here, this File must be an instance variable because it may be used across // multiple calls to startNext. @SuppressWarnings("PMD.SingularField") private File cacheFile; private ResourceCacheKey currentKey; ResourceCacheGenerator(DecodeHelper helper, FetcherReadyCallback cb) { this.helper = helper; this.cb = cb; } // See TODO below. @SuppressWarnings("PMD.CollapsibleIfStatements") @Override public boolean startNext() { GlideTrace.beginSection("ResourceCacheGenerator.startNext"); try { List sourceIds = helper.getCacheKeys(); if (sourceIds.isEmpty()) { return false; } List> resourceClasses = helper.getRegisteredResourceClasses(); if (resourceClasses.isEmpty()) { if (File.class.equals(helper.getTranscodeClass())) { return false; } throw new IllegalStateException( "Failed to find any load path from " + helper.getModelClass() + " to " + helper.getTranscodeClass()); } while (modelLoaders == null || !hasNextModelLoader()) { resourceClassIndex++; if (resourceClassIndex >= resourceClasses.size()) { sourceIdIndex++; if (sourceIdIndex >= sourceIds.size()) { return false; } resourceClassIndex = 0; } Key sourceId = sourceIds.get(sourceIdIndex); Class resourceClass = resourceClasses.get(resourceClassIndex); Transformation transformation = helper.getTransformation(resourceClass); // PMD.AvoidInstantiatingObjectsInLoops Each iteration is comparatively expensive anyway, // we only run until the first one succeeds, the loop runs for only a limited // number of iterations on the order of 10-20 in the worst case. currentKey = new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops helper.getArrayPool(), sourceId, helper.getSignature(), helper.getWidth(), helper.getHeight(), transformation, resourceClass, helper.getOptions()); cacheFile = helper.getDiskCache().get(currentKey); if (cacheFile != null) { sourceKey = sourceId; modelLoaders = helper.getModelLoaders(cacheFile); modelLoaderIndex = 0; } } loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { ModelLoader modelLoader = modelLoaders.get(modelLoaderIndex++); loadData = modelLoader.buildLoadData( cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; } finally { GlideTrace.endSection(); } } private boolean hasNextModelLoader() { return modelLoaderIndex < modelLoaders.size(); } @Override public void cancel() { LoadData local = loadData; if (local != null) { local.fetcher.cancel(); } } @Override public void onDataReady(Object data) { cb.onDataFetcherReady( sourceKey, data, loadData.fetcher, DataSource.RESOURCE_DISK_CACHE, currentKey); } @Override public void onLoadFailed(@NonNull Exception e) { cb.onDataFetcherFailed(currentKey, e, loadData.fetcher, DataSource.RESOURCE_DISK_CACHE); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/ResourceCacheKey.java ================================================ package com.bumptech.glide.load.engine; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.util.LruCache; import com.bumptech.glide.util.Util; import java.nio.ByteBuffer; import java.security.MessageDigest; /** A cache key for downsampled and transformed resource data + any requested signature. */ final class ResourceCacheKey implements Key { private static final LruCache, byte[]> RESOURCE_CLASS_BYTES = new LruCache<>(50); private final ArrayPool arrayPool; private final Key sourceKey; private final Key signature; private final int width; private final int height; private final Class decodedResourceClass; private final Options options; private final Transformation transformation; ResourceCacheKey( ArrayPool arrayPool, Key sourceKey, Key signature, int width, int height, Transformation appliedTransformation, Class decodedResourceClass, Options options) { this.arrayPool = arrayPool; this.sourceKey = sourceKey; this.signature = signature; this.width = width; this.height = height; this.transformation = appliedTransformation; this.decodedResourceClass = decodedResourceClass; this.options = options; } @Override public boolean equals(Object o) { if (o instanceof ResourceCacheKey) { ResourceCacheKey other = (ResourceCacheKey) o; return height == other.height && width == other.width && Util.bothNullOrEqual(transformation, other.transformation) && decodedResourceClass.equals(other.decodedResourceClass) && sourceKey.equals(other.sourceKey) && signature.equals(other.signature) && options.equals(other.options); } return false; } @Override public int hashCode() { int result = sourceKey.hashCode(); result = 31 * result + signature.hashCode(); result = 31 * result + width; result = 31 * result + height; if (transformation != null) { result = 31 * result + transformation.hashCode(); } result = 31 * result + decodedResourceClass.hashCode(); result = 31 * result + options.hashCode(); return result; } // TODO: Include relevant options? @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { byte[] dimensions = arrayPool.getExact(8, byte[].class); ByteBuffer.wrap(dimensions).putInt(width).putInt(height).array(); signature.updateDiskCacheKey(messageDigest); sourceKey.updateDiskCacheKey(messageDigest); messageDigest.update(dimensions); if (transformation != null) { transformation.updateDiskCacheKey(messageDigest); } options.updateDiskCacheKey(messageDigest); messageDigest.update(getResourceClassBytes()); arrayPool.put(dimensions); } private byte[] getResourceClassBytes() { byte[] result = RESOURCE_CLASS_BYTES.get(decodedResourceClass); if (result == null) { result = decodedResourceClass.getName().getBytes(CHARSET); RESOURCE_CLASS_BYTES.put(decodedResourceClass, result); } return result; } @Override public String toString() { return "ResourceCacheKey{" + "sourceKey=" + sourceKey + ", signature=" + signature + ", width=" + width + ", height=" + height + ", decodedResourceClass=" + decodedResourceClass + ", transformation='" + transformation + '\'' + ", options=" + options + '}'; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/ResourceRecycler.java ================================================ package com.bumptech.glide.load.engine; import android.os.Handler; import android.os.Looper; import android.os.Message; import com.bumptech.glide.util.Synthetic; /** A class that can safely recycle recursive resources. */ class ResourceRecycler { private boolean isRecycling; private final Handler handler = new Handler(Looper.getMainLooper(), new ResourceRecyclerCallback()); synchronized void recycle(Resource resource, boolean forceNextFrame) { if (isRecycling || forceNextFrame) { // If a resource has sub-resources, releasing a sub resource can cause it's parent to be // synchronously evicted which leads to a recycle loop when the parent releases it's children. // Posting breaks this loop. handler.obtainMessage(ResourceRecyclerCallback.RECYCLE_RESOURCE, resource).sendToTarget(); } else { isRecycling = true; resource.recycle(); isRecycling = false; } } private static final class ResourceRecyclerCallback implements Handler.Callback { static final int RECYCLE_RESOURCE = 1; @Synthetic ResourceRecyclerCallback() {} @Override public boolean handleMessage(Message message) { if (message.what == RECYCLE_RESOURCE) { Resource resource = (Resource) message.obj; resource.recycle(); return true; } return false; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/SourceGenerator.java ================================================ package com.bumptech.glide.load.engine; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Encoder; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.DataFetcher.DataCallback; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoader.LoadData; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Synthetic; import java.io.IOException; import java.util.Collections; /** * Generates {@link com.bumptech.glide.load.data.DataFetcher DataFetchers} from original source data * using registered {@link com.bumptech.glide.load.model.ModelLoader ModelLoaders} and the model * provided for the load. * *

    Depending on the disk cache strategy, source data may first be written to disk and then loaded * from the cache file rather than returned directly. * *

    This object may be used by multiple threads, but only one at a time. It is not safe to access * this object on multiple threads concurrently. */ class SourceGenerator implements DataFetcherGenerator, DataFetcherGenerator.FetcherReadyCallback { private static final String TAG = "SourceGenerator"; private final DecodeHelper helper; private final FetcherReadyCallback cb; private volatile int loadDataListIndex; private volatile DataCacheGenerator sourceCacheGenerator; private volatile Object dataToCache; private volatile ModelLoader.LoadData loadData; private volatile DataCacheKey originalKey; SourceGenerator(DecodeHelper helper, FetcherReadyCallback cb) { this.helper = helper; this.cb = cb; } // Concurrent access isn't supported. @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) @Override public boolean startNext() { if (dataToCache != null) { Object data = dataToCache; dataToCache = null; try { boolean isDataInCache = cacheData(data); // If we failed to write the data to cache, the cacheData method will try to decode the // original data directly instead of going through the disk cache. Since cacheData has // already called our callback at this point, there's nothing more to do but return. if (!isDataInCache) { return true; } // If we were able to write the data to cache successfully, we now need to proceed to call // the sourceCacheGenerator below to load the data from cache. } catch (IOException e) { // An IOException means we weren't able to write data to cache or we weren't able to rewind // it after a disk cache write failed. In either case we can just move on and try the next // fetch below. if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to properly rewind or write data to cache", e); } } } if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) { return true; } sourceCacheGenerator = null; loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { loadData = helper.getLoadData().get(loadDataListIndex++); if (loadData != null && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource()) || helper.hasLoadPath(loadData.fetcher.getDataClass()))) { started = true; startNextLoad(loadData); } } return started; } private void startNextLoad(final LoadData toStart) { loadData.fetcher.loadData( helper.getPriority(), new DataCallback() { @Override public void onDataReady(@Nullable Object data) { if (isCurrentRequest(toStart)) { onDataReadyInternal(toStart, data); } } @Override public void onLoadFailed(@NonNull Exception e) { if (isCurrentRequest(toStart)) { onLoadFailedInternal(toStart, e); } } }); } // We want reference equality explicitly to make sure we ignore results from old requests. @SuppressWarnings({"PMD.CompareObjectsWithEquals", "WeakerAccess"}) @Synthetic boolean isCurrentRequest(LoadData requestLoadData) { LoadData currentLoadData = loadData; return currentLoadData != null && currentLoadData == requestLoadData; } private boolean hasNextModelLoader() { return loadDataListIndex < helper.getLoadData().size(); } /** * Returns {@code true} if we were able to cache the data and should try to decode the data * directly from cache and {@code false} if we were unable to cache the data and should make an * attempt to decode from source. */ private boolean cacheData(Object dataToCache) throws IOException { long startTime = LogTime.getLogTime(); boolean isLoadingFromSourceData = false; try { DataRewinder rewinder = helper.getRewinder(dataToCache); Object data = rewinder.rewindAndGet(); Encoder encoder = helper.getSourceEncoder(data); DataCacheWriter writer = new DataCacheWriter<>(encoder, data, helper.getOptions()); DataCacheKey newOriginalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature()); DiskCache diskCache = helper.getDiskCache(); diskCache.put(newOriginalKey, writer); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Finished encoding source to cache" + ", key: " + newOriginalKey + ", data: " + dataToCache + ", encoder: " + encoder + ", duration: " + LogTime.getElapsedMillis(startTime)); } if (diskCache.get(newOriginalKey) != null) { originalKey = newOriginalKey; sourceCacheGenerator = new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this); // We were able to write the data to cache. return true; } else { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Attempt to write: " + originalKey + ", data: " + dataToCache + " to the disk" + " cache failed, maybe the disk cache is disabled?" + " Trying to decode the data directly..."); } isLoadingFromSourceData = true; cb.onDataFetcherReady( loadData.sourceKey, rewinder.rewindAndGet(), loadData.fetcher, loadData.fetcher.getDataSource(), loadData.sourceKey); } // We failed to write the data to cache. return false; } finally { if (!isLoadingFromSourceData) { loadData.fetcher.cleanup(); } } } @Override public void cancel() { LoadData local = loadData; if (local != null) { local.fetcher.cancel(); } } @SuppressWarnings("WeakerAccess") @Synthetic void onDataReadyInternal(LoadData loadData, Object data) { DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy(); if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) { dataToCache = data; // We might be being called back on someone else's thread. Before doing anything, we should // reschedule to get back onto Glide's thread. Then once we're back on Glide's thread, we'll // get called again and we can write the retrieved data to cache. cb.reschedule(); } else { cb.onDataFetcherReady( loadData.sourceKey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey); } } @SuppressWarnings("WeakerAccess") @Synthetic void onLoadFailedInternal(LoadData loadData, @NonNull Exception e) { cb.onDataFetcherFailed(originalKey, e, loadData.fetcher, loadData.fetcher.getDataSource()); } @Override public void reschedule() { // We don't expect this to happen, although if we ever need it to we can delegate to our // callback. throw new UnsupportedOperationException(); } // Called from source cache generator. @Override public void onDataFetcherReady( Key sourceKey, Object data, DataFetcher fetcher, DataSource dataSource, Key attemptedKey) { // This data fetcher will be loading from a File and provide the wrong data source, so override // with the data source of the original fetcher cb.onDataFetcherReady(sourceKey, data, fetcher, loadData.fetcher.getDataSource(), sourceKey); } @Override public void onDataFetcherFailed( Key sourceKey, Exception e, DataFetcher fetcher, DataSource dataSource) { cb.onDataFetcherFailed(sourceKey, e, fetcher, loadData.fetcher.getDataSource()); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/ArrayAdapterInterface.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; /** * Interface for handling operations on a primitive array type. * * @param Array type (e.g. byte[], int[]) */ interface ArrayAdapterInterface { /** TAG for logging. */ String getTag(); /** Return the length of the given array. */ int getArrayLength(T array); /** Allocate and return an array of the specified size. */ T newArray(int length); /** Return the size of an element in the array in bytes (e.g. for int return 4). */ int getElementSizeInBytes(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/ArrayPool.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; /** Interface for an array pool that pools arrays of different types. */ public interface ArrayPool { /** * A standard size to use to increase hit rates when the required size isn't defined. Currently * 64KB. */ int STANDARD_BUFFER_SIZE_BYTES = 64 * 1024; /** * Optionally adds the given array of the given type to the pool. * *

    Arrays may be ignored, for example if the array is larger than the maximum size of the pool. * * @deprecated Use {@link #put(Object)} */ @Deprecated void put(T array, Class arrayClass); /** * Optionally adds the given array of the given type to the pool. * *

    Arrays may be ignored, for example if the array is larger than the maximum size of the pool. */ void put(T array); /** * Returns a non-null array of the given type with a length {@code >=} to the given size. * *

    If an array of the given size isn't in the pool, a new one will be allocated. * *

    This class makes no guarantees about the contents of the returned array. * * @see #getExact(int, Class) */ T get(int size, Class arrayClass); /** * Returns a non-null array of the given type with a length exactly equal to the given size. * *

    If an array of the given size isn't in the pool, a new one will be allocated. * *

    This class makes no guarantees about the contents of the returned array. * * @see #get(int, Class) */ T getExact(int size, Class arrayClass); /** Clears all arrays from the pool. */ void clearMemory(); /** * Trims the size to the appropriate level. * * @param level A trim specified in {@link android.content.ComponentCallbacks2}. */ void trimMemory(int level); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/AttributeStrategy.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.graphics.Bitmap; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; /** * A strategy for reusing bitmaps that requires any returned bitmap's dimensions to exactly match * those request. */ class AttributeStrategy implements LruPoolStrategy { private final KeyPool keyPool = new KeyPool(); private final GroupedLinkedMap groupedMap = new GroupedLinkedMap<>(); @Override public void put(Bitmap bitmap) { final Key key = keyPool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); groupedMap.put(key, bitmap); } @Override public Bitmap get(int width, int height, Bitmap.Config config) { final Key key = keyPool.get(width, height, config); return groupedMap.get(key); } @Override public Bitmap removeLast() { return groupedMap.removeLast(); } @Override public String logBitmap(Bitmap bitmap) { return getBitmapString(bitmap); } @Override public String logBitmap(int width, int height, Bitmap.Config config) { return getBitmapString(width, height, config); } @Override public int getSize(Bitmap bitmap) { return Util.getBitmapByteSize(bitmap); } @Override public String toString() { return "AttributeStrategy:\n " + groupedMap; } private static String getBitmapString(Bitmap bitmap) { return getBitmapString(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); } @SuppressWarnings("WeakerAccess") @Synthetic static String getBitmapString(int width, int height, Bitmap.Config config) { return "[" + width + "x" + height + "], " + config; } @VisibleForTesting static class KeyPool extends BaseKeyPool { Key get(int width, int height, Bitmap.Config config) { Key result = get(); result.init(width, height, config); return result; } @Override protected Key create() { return new Key(this); } } @VisibleForTesting static class Key implements Poolable { private final KeyPool pool; private int width; private int height; // Config can be null :( private Bitmap.Config config; public Key(KeyPool pool) { this.pool = pool; } public void init(int width, int height, Bitmap.Config config) { this.width = width; this.height = height; this.config = config; } @Override public boolean equals(Object o) { if (o instanceof Key) { Key other = (Key) o; return width == other.width && height == other.height && config == other.config; } return false; } @Override public int hashCode() { int result = width; result = 31 * result + height; result = 31 * result + (config != null ? config.hashCode() : 0); return result; } @Override public String toString() { return getBitmapString(width, height, config); } @Override public void offer() { pool.offer(this); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/BaseKeyPool.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import com.bumptech.glide.util.Util; import java.util.Queue; abstract class BaseKeyPool { private static final int MAX_SIZE = 20; private final Queue keyPool = Util.createQueue(MAX_SIZE); T get() { T result = keyPool.poll(); if (result == null) { result = create(); } return result; } public void offer(T key) { if (keyPool.size() < MAX_SIZE) { keyPool.offer(key); } } abstract T create(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/BitmapPool.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.graphics.Bitmap; import androidx.annotation.NonNull; /** An interface for a pool that allows users to reuse {@link android.graphics.Bitmap} objects. */ public interface BitmapPool { /** Returns the current maximum size of the pool in bytes. */ long getMaxSize(); /** * Multiplies the initial size of the pool by the given multiplier to dynamically and * synchronously allow users to adjust the size of the pool. * *

    If the current total size of the pool is larger than the max size after the given multiplier * is applied, {@link Bitmap}s should be evicted until the pool is smaller than the new max size. * * @param sizeMultiplier The size multiplier to apply between 0 and 1. */ void setSizeMultiplier(float sizeMultiplier); /** * Adds the given {@link android.graphics.Bitmap} if it is eligible to be re-used and the pool can * fit it, or calls {@link Bitmap#recycle()} on the Bitmap and discards it. * *

    Callers must not continue to use the Bitmap after calling this method. * * @param bitmap The {@link android.graphics.Bitmap} to attempt to add. * @see android.graphics.Bitmap#isMutable() * @see android.graphics.Bitmap#recycle() */ void put(Bitmap bitmap); /** * Returns a {@link android.graphics.Bitmap} of exactly the given width, height, and * configuration, and containing only transparent pixels. * *

    If no Bitmap with the requested attributes is present in the pool, a new one will be * allocated. * *

    Because this method erases all pixels in the {@link Bitmap}, this method is slightly slower * than {@link #getDirty(int, int, android.graphics.Bitmap.Config)}. If the {@link * android.graphics.Bitmap} is being obtained to be used in {@link android.graphics.BitmapFactory} * or in any other case where every pixel in the {@link android.graphics.Bitmap} will always be * overwritten or cleared, {@link #getDirty(int, int, android.graphics.Bitmap.Config)} will be * faster. When in doubt, use this method to ensure correctness. * *

       *     Implementations can should clear out every returned Bitmap using the following:
       *
       * {@code
       * bitmap.eraseColor(Color.TRANSPARENT);
       * }
       * 
    * * @param width The width in pixels of the desired {@link android.graphics.Bitmap}. * @param height The height in pixels of the desired {@link android.graphics.Bitmap}. * @param config The {@link android.graphics.Bitmap.Config} of the desired {@link * android.graphics.Bitmap}. * @see #getDirty(int, int, android.graphics.Bitmap.Config) */ @NonNull Bitmap get(int width, int height, Bitmap.Config config); /** * Identical to {@link #get(int, int, android.graphics.Bitmap.Config)} except that any returned * {@link android.graphics.Bitmap} may not have been erased and may contain random data. * *

    If no Bitmap with the requested attributes is present in the pool, a new one will be * allocated. * *

    Although this method is slightly more efficient than {@link #get(int, int, * android.graphics.Bitmap.Config)} it should be used with caution and only when the caller is * sure that they are going to erase the {@link android.graphics.Bitmap} entirely before writing * new data to it. * * @param width The width in pixels of the desired {@link android.graphics.Bitmap}. * @param height The height in pixels of the desired {@link android.graphics.Bitmap}. * @param config The {@link android.graphics.Bitmap.Config} of the desired {@link * android.graphics.Bitmap}. * @return A {@link android.graphics.Bitmap} with exactly the given width, height, and config * potentially containing random image data. * @see #get(int, int, android.graphics.Bitmap.Config) */ @NonNull Bitmap getDirty(int width, int height, Bitmap.Config config); /** Removes all {@link android.graphics.Bitmap}s from the pool. */ void clearMemory(); /** * Reduces the size of the cache by evicting items based on the given level. * * @param level The level from {@link android.content.ComponentCallbacks2} to use to determine how * many {@link android.graphics.Bitmap}s to evict. * @see android.content.ComponentCallbacks2 */ void trimMemory(int level); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/BitmapPoolAdapter.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.graphics.Bitmap; import androidx.annotation.NonNull; /** * An {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool BitmapPool} implementation * that rejects all {@link android.graphics.Bitmap Bitmap}s added to it and always returns a new * {@link android.graphics.Bitmap Bitmap} from {@link #get}. */ public class BitmapPoolAdapter implements BitmapPool { @Override public long getMaxSize() { return 0; } @Override public void setSizeMultiplier(float sizeMultiplier) { // Do nothing. } @Override public void put(Bitmap bitmap) { bitmap.recycle(); } @NonNull @Override public Bitmap get(int width, int height, Bitmap.Config config) { return Bitmap.createBitmap(width, height, config); } @NonNull @Override public Bitmap getDirty(int width, int height, Bitmap.Config config) { return get(width, height, config); } @Override public void clearMemory() { // Do nothing. } @Override public void trimMemory(int level) { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/ByteArrayAdapter.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; /** Adapter for handling primitive byte arrays. */ @SuppressWarnings("PMD.UseVarargs") public final class ByteArrayAdapter implements ArrayAdapterInterface { private static final String TAG = "ByteArrayPool"; @Override public String getTag() { return TAG; } @Override public int getArrayLength(byte[] array) { return array.length; } @Override public byte[] newArray(int length) { return new byte[length]; } @Override public int getElementSizeInBytes() { return 1; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/GroupedLinkedMap.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import androidx.annotation.Nullable; import com.bumptech.glide.util.Synthetic; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Similar to {@link java.util.LinkedHashMap} when access ordered except that it is access ordered * on groups of bitmaps rather than individual objects. The idea is to be able to find the LRU * bitmap size, rather than the LRU bitmap object. We can then remove bitmaps from the least * recently used size of bitmap when we need to reduce our cache size. * *

    For the purposes of the LRU, we count gets for a particular size of bitmap as an access, even * if no bitmaps of that size are present. We do not count addition or removal of bitmaps as an * access. */ class GroupedLinkedMap { private final LinkedEntry head = new LinkedEntry<>(); private final Map> keyToEntry = new HashMap<>(); public void put(K key, V value) { LinkedEntry entry = keyToEntry.get(key); if (entry == null) { entry = new LinkedEntry<>(key); makeTail(entry); keyToEntry.put(key, entry); } else { key.offer(); } entry.add(value); } @Nullable public V get(K key) { LinkedEntry entry = keyToEntry.get(key); if (entry == null) { entry = new LinkedEntry<>(key); keyToEntry.put(key, entry); } else { key.offer(); } makeHead(entry); return entry.removeLast(); } @Nullable public V removeLast() { LinkedEntry last = head.prev; while (!last.equals(head)) { V removed = last.removeLast(); if (removed != null) { return removed; } else { // We will clean up empty lru entries since they are likely to have been one off or // unusual sizes and // are not likely to be requested again so the gc thrash should be minimal. Doing so will // speed up our // removeLast operation in the future and prevent our linked list from growing to // arbitrarily large // sizes. removeEntry(last); keyToEntry.remove(last.key); last.key.offer(); } last = last.prev; } return null; } @Override public String toString() { StringBuilder sb = new StringBuilder("GroupedLinkedMap( "); LinkedEntry current = head.next; boolean hadAtLeastOneItem = false; while (!current.equals(head)) { hadAtLeastOneItem = true; sb.append('{').append(current.key).append(':').append(current.size()).append("}, "); current = current.next; } if (hadAtLeastOneItem) { sb.delete(sb.length() - 2, sb.length()); } return sb.append(" )").toString(); } // Make the entry the most recently used item. private void makeHead(LinkedEntry entry) { removeEntry(entry); entry.prev = head; entry.next = head.next; updateEntry(entry); } // Make the entry the least recently used item. private void makeTail(LinkedEntry entry) { removeEntry(entry); entry.prev = head.prev; entry.next = head; updateEntry(entry); } private static void updateEntry(LinkedEntry entry) { entry.next.prev = entry; entry.prev.next = entry; } private static void removeEntry(LinkedEntry entry) { entry.prev.next = entry.next; entry.next.prev = entry.prev; } private static class LinkedEntry { @Synthetic final K key; private List values; LinkedEntry next; LinkedEntry prev; // Used only for the first item in the list which we will treat specially and which will not // contain a value. LinkedEntry() { this(null); } LinkedEntry(K key) { next = prev = this; this.key = key; } @Nullable public V removeLast() { final int valueSize = size(); return valueSize > 0 ? values.remove(valueSize - 1) : null; } public int size() { return values != null ? values.size() : 0; } public void add(V value) { if (values == null) { values = new ArrayList<>(); } values.add(value); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/IntegerArrayAdapter.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; /** Adapter for handling primitive int arrays. */ @SuppressWarnings("PMD.UseVarargs") public final class IntegerArrayAdapter implements ArrayAdapterInterface { private static final String TAG = "IntegerArrayPool"; @Override public String getTag() { return TAG; } @Override public int getArrayLength(int[] array) { return array.length; } @Override public int[] newArray(int length) { return new int[length]; } @Override public int getElementSizeInBytes() { return 4; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/LruArrayPool.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; /** * A fixed size Array Pool that evicts arrays using an LRU strategy to keep the pool under the * maximum byte size. */ public final class LruArrayPool implements ArrayPool { // 4MB. private static final int DEFAULT_SIZE = 4 * 1024 * 1024; /** * The maximum number of times larger an int array may be to be than a requested size to eligible * to be returned from the pool. */ @VisibleForTesting static final int MAX_OVER_SIZE_MULTIPLE = 8; /** Used to calculate the maximum % of the total pool size a single byte array may consume. */ private static final int SINGLE_ARRAY_MAX_SIZE_DIVISOR = 2; private final GroupedLinkedMap groupedMap = new GroupedLinkedMap<>(); private final KeyPool keyPool = new KeyPool(); private final Map, NavigableMap> sortedSizes = new HashMap<>(); private final Map, ArrayAdapterInterface> adapters = new HashMap<>(); private final int maxSize; private int currentSize; @VisibleForTesting public LruArrayPool() { maxSize = DEFAULT_SIZE; } /** * Constructor for a new pool. * * @param maxSize The maximum size in integers of the pool. */ public LruArrayPool(int maxSize) { this.maxSize = maxSize; } @Deprecated @Override public void put(T array, Class arrayClass) { put(array); } @Override public synchronized void put(T array) { @SuppressWarnings("unchecked") Class arrayClass = (Class) array.getClass(); ArrayAdapterInterface arrayAdapter = getAdapterFromType(arrayClass); int size = arrayAdapter.getArrayLength(array); int arrayBytes = size * arrayAdapter.getElementSizeInBytes(); if (!isSmallEnoughForReuse(arrayBytes)) { return; } Key key = keyPool.get(size, arrayClass); groupedMap.put(key, array); NavigableMap sizes = getSizesForAdapter(arrayClass); Integer current = sizes.get(key.size); sizes.put(key.size, current == null ? 1 : current + 1); currentSize += arrayBytes; evict(); } @Override public synchronized T getExact(int size, Class arrayClass) { Key key = keyPool.get(size, arrayClass); return getForKey(key, arrayClass); } @Override public synchronized T get(int size, Class arrayClass) { Integer possibleSize = getSizesForAdapter(arrayClass).ceilingKey(size); final Key key; if (mayFillRequest(size, possibleSize)) { key = keyPool.get(possibleSize, arrayClass); } else { key = keyPool.get(size, arrayClass); } return getForKey(key, arrayClass); } private T getForKey(Key key, Class arrayClass) { ArrayAdapterInterface arrayAdapter = getAdapterFromType(arrayClass); T result = getArrayForKey(key); if (result != null) { currentSize -= arrayAdapter.getArrayLength(result) * arrayAdapter.getElementSizeInBytes(); decrementArrayOfSize(arrayAdapter.getArrayLength(result), arrayClass); } if (result == null) { if (Log.isLoggable(arrayAdapter.getTag(), Log.VERBOSE)) { Log.v(arrayAdapter.getTag(), "Allocated " + key.size + " bytes"); } result = arrayAdapter.newArray(key.size); } return result; } // Our cast is safe because the Key is based on the type. @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) @Nullable private T getArrayForKey(Key key) { return (T) groupedMap.get(key); } private boolean isSmallEnoughForReuse(int byteSize) { return byteSize <= maxSize / SINGLE_ARRAY_MAX_SIZE_DIVISOR; } private boolean mayFillRequest(int requestedSize, Integer actualSize) { return actualSize != null && (isNoMoreThanHalfFull() || actualSize <= (MAX_OVER_SIZE_MULTIPLE * requestedSize)); } private boolean isNoMoreThanHalfFull() { return currentSize == 0 || maxSize / currentSize >= 2; } @Override public synchronized void clearMemory() { evictToSize(0); } @Override public synchronized void trimMemory(int level) { if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { clearMemory(); } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { evictToSize(maxSize / 2); } } private void evict() { evictToSize(maxSize); } private void evictToSize(int size) { while (currentSize > size) { Object evicted = groupedMap.removeLast(); Preconditions.checkNotNull(evicted); ArrayAdapterInterface arrayAdapter = getAdapterFromObject(evicted); currentSize -= arrayAdapter.getArrayLength(evicted) * arrayAdapter.getElementSizeInBytes(); decrementArrayOfSize(arrayAdapter.getArrayLength(evicted), evicted.getClass()); if (Log.isLoggable(arrayAdapter.getTag(), Log.VERBOSE)) { Log.v(arrayAdapter.getTag(), "evicted: " + arrayAdapter.getArrayLength(evicted)); } } } private void decrementArrayOfSize(int size, Class arrayClass) { NavigableMap sizes = getSizesForAdapter(arrayClass); Integer current = sizes.get(size); if (current == null) { throw new NullPointerException( "Tried to decrement empty size" + ", size: " + size + ", this: " + this); } if (current == 1) { sizes.remove(size); } else { sizes.put(size, current - 1); } } private NavigableMap getSizesForAdapter(Class arrayClass) { NavigableMap sizes = sortedSizes.get(arrayClass); if (sizes == null) { sizes = new TreeMap<>(); sortedSizes.put(arrayClass, sizes); } return sizes; } @SuppressWarnings("unchecked") private ArrayAdapterInterface getAdapterFromObject(T object) { return (ArrayAdapterInterface) getAdapterFromType(object.getClass()); } @SuppressWarnings("unchecked") private ArrayAdapterInterface getAdapterFromType(Class arrayPoolClass) { ArrayAdapterInterface adapter = adapters.get(arrayPoolClass); if (adapter == null) { if (arrayPoolClass.equals(int[].class)) { adapter = new IntegerArrayAdapter(); } else if (arrayPoolClass.equals(byte[].class)) { adapter = new ByteArrayAdapter(); } else { throw new IllegalArgumentException( "No array pool found for: " + arrayPoolClass.getSimpleName()); } adapters.put(arrayPoolClass, adapter); } return (ArrayAdapterInterface) adapter; } // VisibleForTesting int getCurrentSize() { int currentSize = 0; for (Class type : sortedSizes.keySet()) { for (Integer size : sortedSizes.get(type).keySet()) { ArrayAdapterInterface adapter = getAdapterFromType(type); currentSize += size * sortedSizes.get(type).get(size) * adapter.getElementSizeInBytes(); } } return currentSize; } private static final class KeyPool extends BaseKeyPool { @Synthetic KeyPool() {} Key get(int size, Class arrayClass) { Key result = get(); result.init(size, arrayClass); return result; } @Override protected Key create() { return new Key(this); } } private static final class Key implements Poolable { private final KeyPool pool; @Synthetic int size; private Class arrayClass; Key(KeyPool pool) { this.pool = pool; } void init(int length, Class arrayClass) { this.size = length; this.arrayClass = arrayClass; } @Override public boolean equals(Object o) { if (o instanceof Key) { Key other = (Key) o; return size == other.size && arrayClass == other.arrayClass; } return false; } @Override public String toString() { return "Key{" + "size=" + size + "array=" + arrayClass + '}'; } @Override public void offer() { pool.offer(this); } @Override public int hashCode() { int result = size; result = 31 * result + (arrayClass != null ? arrayClass.hashCode() : 0); return result; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/LruBitmapPool.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ComponentCallbacks2; import android.graphics.Bitmap; import android.graphics.Color; import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.util.Synthetic; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * An {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} implementation that uses an * {@link com.bumptech.glide.load.engine.bitmap_recycle.LruPoolStrategy} to bucket {@link Bitmap}s * and then uses an LRU eviction policy to evict {@link android.graphics.Bitmap}s from the least * recently used bucket in order to keep the pool below a given maximum size limit. */ public class LruBitmapPool implements BitmapPool { private static final String TAG = "LruBitmapPool"; private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888; private final LruPoolStrategy strategy; private final Set allowedConfigs; private final long initialMaxSize; private final BitmapTracker tracker; private long maxSize; private long currentSize; private int hits; private int misses; private int puts; private int evictions; // Exposed for testing only. LruBitmapPool(long maxSize, LruPoolStrategy strategy, Set allowedConfigs) { this.initialMaxSize = maxSize; this.maxSize = maxSize; this.strategy = strategy; this.allowedConfigs = allowedConfigs; this.tracker = new NullBitmapTracker(); } /** * Constructor for LruBitmapPool. * * @param maxSize The initial maximum size of the pool in bytes. */ public LruBitmapPool(long maxSize) { this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs()); } /** * Constructor for LruBitmapPool. * * @param maxSize The initial maximum size of the pool in bytes. * @param allowedConfigs A white listed put of {@link android.graphics.Bitmap.Config} that are * allowed to be put into the pool. Configs not in the allowed put will be rejected. */ // Public API. @SuppressWarnings("unused") public LruBitmapPool(long maxSize, Set allowedConfigs) { this(maxSize, getDefaultStrategy(), allowedConfigs); } /** Returns the number of cache hits for bitmaps in the pool. */ public long hitCount() { return hits; } /** Returns the number of cache misses for bitmaps in the pool. */ public long missCount() { return misses; } /** Returns the number of bitmaps that have been evicted from the pool. */ public long evictionCount() { return evictions; } /** Returns the current size of the pool in bytes. */ public long getCurrentSize() { return currentSize; } @Override public long getMaxSize() { return maxSize; } @Override public synchronized void setSizeMultiplier(float sizeMultiplier) { maxSize = Math.round(initialMaxSize * sizeMultiplier); evict(); } @Override public synchronized void put(Bitmap bitmap) { if (bitmap == null) { throw new NullPointerException("Bitmap must not be null"); } if (bitmap.isRecycled()) { throw new IllegalStateException("Cannot pool recycled bitmap"); } if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize || !allowedConfigs.contains(bitmap.getConfig())) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Reject bitmap from pool" + ", bitmap: " + strategy.logBitmap(bitmap) + ", is mutable: " + bitmap.isMutable() + ", is allowed config: " + allowedConfigs.contains(bitmap.getConfig())); } bitmap.recycle(); return; } final int size = strategy.getSize(bitmap); strategy.put(bitmap); tracker.add(bitmap); puts++; currentSize += size; if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap)); } dump(); evict(); } private void evict() { trimToSize(maxSize); } @Override @NonNull public Bitmap get(int width, int height, Bitmap.Config config) { Bitmap result = getDirtyOrNull(width, height, config); if (result != null) { // Bitmaps in the pool contain random data that in some cases must be cleared for an image // to be rendered correctly. we shouldn't force all consumers to independently erase the // contents individually, so we do so here. See issue #131. result.eraseColor(Color.TRANSPARENT); } else { result = createBitmap(width, height, config); } return result; } @NonNull @Override public Bitmap getDirty(int width, int height, Bitmap.Config config) { Bitmap result = getDirtyOrNull(width, height, config); if (result == null) { result = createBitmap(width, height, config); } return result; } @NonNull private static Bitmap createBitmap(int width, int height, @Nullable Bitmap.Config config) { return Bitmap.createBitmap(width, height, config != null ? config : DEFAULT_CONFIG); } @TargetApi(Build.VERSION_CODES.O) private static void assertNotHardwareConfig(Bitmap.Config config) { // Avoid short circuiting on sdk int since it breaks on some versions of Android. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return; } if (config == Bitmap.Config.HARDWARE) { throw new IllegalArgumentException( "Cannot create a mutable Bitmap with config: " + config + ". Consider setting Downsampler#ALLOW_HARDWARE_CONFIG to false in your" + " RequestOptions and/or in GlideBuilder.setDefaultRequestOptions"); } } @Nullable private synchronized Bitmap getDirtyOrNull( int width, int height, @Nullable Bitmap.Config config) { assertNotHardwareConfig(config); // Config will be null for non public config types, which can lead to transformations naively // passing in null as the requested config here. See issue #194. final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG); if (result == null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config)); } misses++; } else { hits++; currentSize -= strategy.getSize(result); tracker.remove(result); normalize(result); } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config)); } dump(); return result; } // Setting these two values provides Bitmaps that are essentially equivalent to those returned // from Bitmap.createBitmap. private static void normalize(Bitmap bitmap) { bitmap.setHasAlpha(true); maybeSetPreMultiplied(bitmap); } @TargetApi(Build.VERSION_CODES.KITKAT) private static void maybeSetPreMultiplied(Bitmap bitmap) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { bitmap.setPremultiplied(true); } } @Override public void clearMemory() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "clearMemory"); } trimToSize(0); } @SuppressWarnings("checkstyle:UnnecessaryParentheses") // Readability @SuppressLint("InlinedApi") @Override public void trimMemory(int level) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "trimMemory, level=" + level); } if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)) { clearMemory(); } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN || level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { trimToSize(getMaxSize() / 2); } } private synchronized void trimToSize(long size) { while (currentSize > size) { final Bitmap removed = strategy.removeLast(); // TODO: This shouldn't ever happen, see #331. if (removed == null) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Size mismatch, resetting"); dumpUnchecked(); } currentSize = 0; return; } tracker.remove(removed); currentSize -= strategy.getSize(removed); evictions++; if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed)); } dump(); removed.recycle(); } } private void dump() { if (Log.isLoggable(TAG, Log.VERBOSE)) { dumpUnchecked(); } } private void dumpUnchecked() { Log.v( TAG, "Hits=" + hits + ", misses=" + misses + ", puts=" + puts + ", evictions=" + evictions + ", currentSize=" + currentSize + ", maxSize=" + maxSize + "\nStrategy=" + strategy); } private static LruPoolStrategy getDefaultStrategy() { final LruPoolStrategy strategy; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { strategy = new SizeConfigStrategy(); } else { strategy = new AttributeStrategy(); } return strategy; } @TargetApi(Build.VERSION_CODES.O) private static Set getDefaultAllowedConfigs() { Set configs = new HashSet<>(Arrays.asList(Bitmap.Config.values())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // GIFs, among other types, end up with a native Bitmap config that doesn't map to a java // config and is treated as null in java code. On KitKat+ these Bitmaps can be reconfigured // and are suitable for re-use. configs.add(null); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { configs.remove(Bitmap.Config.HARDWARE); } return Collections.unmodifiableSet(configs); } private interface BitmapTracker { void add(Bitmap bitmap); void remove(Bitmap bitmap); } @SuppressWarnings("unused") // Only used for debugging private static class ThrowingBitmapTracker implements BitmapTracker { private final Set bitmaps = Collections.synchronizedSet(new HashSet()); @Override public void add(Bitmap bitmap) { if (bitmaps.contains(bitmap)) { throw new IllegalStateException( "Can't add already added bitmap: " + bitmap + " [" + bitmap.getWidth() + "x" + bitmap.getHeight() + "]"); } bitmaps.add(bitmap); } @Override public void remove(Bitmap bitmap) { if (!bitmaps.contains(bitmap)) { throw new IllegalStateException("Cannot remove bitmap not in tracker"); } bitmaps.remove(bitmap); } } private static final class NullBitmapTracker implements BitmapTracker { @Synthetic NullBitmapTracker() {} @Override public void add(Bitmap bitmap) { // Do nothing. } @Override public void remove(Bitmap bitmap) { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/LruPoolStrategy.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.graphics.Bitmap; import androidx.annotation.Nullable; interface LruPoolStrategy { void put(Bitmap bitmap); @Nullable Bitmap get(int width, int height, Bitmap.Config config); @Nullable Bitmap removeLast(); String logBitmap(Bitmap bitmap); String logBitmap(int width, int height, Bitmap.Config config); int getSize(Bitmap bitmap); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/Poolable.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; interface Poolable { void offer(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/PrettyPrintTreeMap.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import java.util.TreeMap; // Never serialized. @SuppressWarnings("serial") class PrettyPrintTreeMap extends TreeMap { @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("( "); for (Entry entry : entrySet()) { sb.append('{').append(entry.getKey()).append(':').append(entry.getValue()).append("}, "); } if (!isEmpty()) { sb.replace(sb.length() - 2, sb.length(), ""); } return sb.append(" )").toString(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/SizeConfigStrategy.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.os.Build; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; /** * Keys {@link android.graphics.Bitmap Bitmaps} using both {@link * android.graphics.Bitmap#getAllocationByteCount()} and the {@link android.graphics.Bitmap.Config} * returned from {@link android.graphics.Bitmap#getConfig()}. * *

    Using both the config and the byte size allows us to safely re-use a greater variety of {@link * android.graphics.Bitmap Bitmaps}, which increases the hit rate of the pool and therefore the * performance of applications. This class works around #301 by only allowing re-use of {@link * android.graphics.Bitmap Bitmaps} with a matching number of bytes per pixel. */ @RequiresApi(Build.VERSION_CODES.KITKAT) public class SizeConfigStrategy implements LruPoolStrategy { private static final int MAX_SIZE_MULTIPLE = 8; private static final Bitmap.Config[] ARGB_8888_IN_CONFIGS; static { Bitmap.Config[] result = new Bitmap.Config[] { Bitmap.Config.ARGB_8888, // The value returned by Bitmaps with the hidden Bitmap config. null, }; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { result = Arrays.copyOf(result, result.length + 1); result[result.length - 1] = Config.RGBA_F16; } ARGB_8888_IN_CONFIGS = result; } private static final Bitmap.Config[] RGBA_F16_IN_CONFIGS = ARGB_8888_IN_CONFIGS; // We probably could allow ARGB_4444 and RGB_565 to decode into each other, but ARGB_4444 is // deprecated and we'd rather be safe. private static final Bitmap.Config[] RGB_565_IN_CONFIGS = new Bitmap.Config[] {Bitmap.Config.RGB_565}; private static final Bitmap.Config[] ARGB_4444_IN_CONFIGS = new Bitmap.Config[] {Bitmap.Config.ARGB_4444}; private static final Bitmap.Config[] ALPHA_8_IN_CONFIGS = new Bitmap.Config[] {Bitmap.Config.ALPHA_8}; private final KeyPool keyPool = new KeyPool(); private final GroupedLinkedMap groupedMap = new GroupedLinkedMap<>(); private final Map> sortedSizes = new HashMap<>(); @Override public void put(Bitmap bitmap) { int size = Util.getBitmapByteSize(bitmap); Key key = keyPool.get(size, bitmap.getConfig()); groupedMap.put(key, bitmap); NavigableMap sizes = getSizesForConfig(bitmap.getConfig()); Integer current = sizes.get(key.size); sizes.put(key.size, current == null ? 1 : current + 1); } @Override @Nullable public Bitmap get(int width, int height, Bitmap.Config config) { int size = Util.getBitmapByteSize(width, height, config); Key bestKey = findBestKey(size, config); Bitmap result = groupedMap.get(bestKey); if (result != null) { // Decrement must be called before reconfigure. decrementBitmapOfSize(bestKey.size, result); result.reconfigure(width, height, config); } return result; } private Key findBestKey(int size, Bitmap.Config config) { Key result = keyPool.get(size, config); for (Bitmap.Config possibleConfig : getInConfigs(config)) { NavigableMap sizesForPossibleConfig = getSizesForConfig(possibleConfig); Integer possibleSize = sizesForPossibleConfig.ceilingKey(size); if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) { if (possibleSize != size || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) { keyPool.offer(result); result = keyPool.get(possibleSize, possibleConfig); } break; } } return result; } @Override @Nullable public Bitmap removeLast() { Bitmap removed = groupedMap.removeLast(); if (removed != null) { int removedSize = Util.getBitmapByteSize(removed); decrementBitmapOfSize(removedSize, removed); } return removed; } private void decrementBitmapOfSize(Integer size, Bitmap removed) { Bitmap.Config config = removed.getConfig(); NavigableMap sizes = getSizesForConfig(config); Integer current = sizes.get(size); if (current == null) { throw new NullPointerException( "Tried to decrement empty size" + ", size: " + size + ", removed: " + logBitmap(removed) + ", this: " + this); } if (current == 1) { sizes.remove(size); } else { sizes.put(size, current - 1); } } private NavigableMap getSizesForConfig(Bitmap.Config config) { NavigableMap sizes = sortedSizes.get(config); if (sizes == null) { sizes = new TreeMap<>(); sortedSizes.put(config, sizes); } return sizes; } @Override public String logBitmap(Bitmap bitmap) { int size = Util.getBitmapByteSize(bitmap); return getBitmapString(size, bitmap.getConfig()); } @Override public String logBitmap(int width, int height, Bitmap.Config config) { int size = Util.getBitmapByteSize(width, height, config); return getBitmapString(size, config); } @Override public int getSize(Bitmap bitmap) { return Util.getBitmapByteSize(bitmap); } @Override public String toString() { StringBuilder sb = new StringBuilder() .append("SizeConfigStrategy{groupedMap=") .append(groupedMap) .append(", sortedSizes=("); for (Map.Entry> entry : sortedSizes.entrySet()) { sb.append(entry.getKey()).append('[').append(entry.getValue()).append("], "); } if (!sortedSizes.isEmpty()) { sb.replace(sb.length() - 2, sb.length(), ""); } return sb.append(")}").toString(); } @VisibleForTesting static class KeyPool extends BaseKeyPool { public Key get(int size, Bitmap.Config config) { Key result = get(); result.init(size, config); return result; } @Override protected Key create() { return new Key(this); } } @VisibleForTesting static final class Key implements Poolable { private final KeyPool pool; @Synthetic int size; private Bitmap.Config config; public Key(KeyPool pool) { this.pool = pool; } @VisibleForTesting Key(KeyPool pool, int size, Bitmap.Config config) { this(pool); init(size, config); } public void init(int size, Bitmap.Config config) { this.size = size; this.config = config; } @Override public void offer() { pool.offer(this); } @Override public String toString() { return getBitmapString(size, config); } @Override public boolean equals(Object o) { if (o instanceof Key) { Key other = (Key) o; return size == other.size && Util.bothNullOrEqual(config, other.config); } return false; } @Override public int hashCode() { int result = size; result = 31 * result + (config != null ? config.hashCode() : 0); return result; } } @Synthetic static String getBitmapString(int size, Bitmap.Config config) { return "[" + size + "](" + config + ")"; } private static Bitmap.Config[] getInConfigs(Bitmap.Config requested) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Bitmap.Config.RGBA_F16.equals(requested)) { // NOPMD - Avoid short circuiting sdk checks. return RGBA_F16_IN_CONFIGS; } } switch (requested) { case ARGB_8888: return ARGB_8888_IN_CONFIGS; case RGB_565: return RGB_565_IN_CONFIGS; case ARGB_4444: return ARGB_4444_IN_CONFIGS; case ALPHA_8: return ALPHA_8_IN_CONFIGS; default: return new Bitmap.Config[] {requested}; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/bitmap_recycle/SizeStrategy.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.graphics.Bitmap; import android.os.Build; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.util.NavigableMap; /** * A strategy for reusing bitmaps that relies on {@link Bitmap#reconfigure(int, int, * Bitmap.Config)}. * *

    Requires {@link Build.VERSION_CODES#KITKAT KitKat} or higher. */ @RequiresApi(Build.VERSION_CODES.KITKAT) final class SizeStrategy implements LruPoolStrategy { private static final int MAX_SIZE_MULTIPLE = 8; private final KeyPool keyPool = new KeyPool(); private final GroupedLinkedMap groupedMap = new GroupedLinkedMap<>(); private final NavigableMap sortedSizes = new PrettyPrintTreeMap<>(); @Override public void put(Bitmap bitmap) { int size = Util.getBitmapByteSize(bitmap); final Key key = keyPool.get(size); groupedMap.put(key, bitmap); Integer current = sortedSizes.get(key.size); sortedSizes.put(key.size, current == null ? 1 : current + 1); } @Override @Nullable public Bitmap get(int width, int height, Bitmap.Config config) { final int size = Util.getBitmapByteSize(width, height, config); Key key = keyPool.get(size); Integer possibleSize = sortedSizes.ceilingKey(size); if (possibleSize != null && possibleSize != size && possibleSize <= size * MAX_SIZE_MULTIPLE) { keyPool.offer(key); key = keyPool.get(possibleSize); } // Do a get even if we know we don't have a bitmap so that the key moves to the front in the // lru pool final Bitmap result = groupedMap.get(key); if (result != null) { result.reconfigure(width, height, config); decrementBitmapOfSize(possibleSize); } return result; } @Override @Nullable public Bitmap removeLast() { Bitmap removed = groupedMap.removeLast(); if (removed != null) { final int removedSize = Util.getBitmapByteSize(removed); decrementBitmapOfSize(removedSize); } return removed; } private void decrementBitmapOfSize(Integer size) { Integer current = sortedSizes.get(size); if (current == 1) { sortedSizes.remove(size); } else { sortedSizes.put(size, current - 1); } } @Override public String logBitmap(Bitmap bitmap) { return getBitmapString(bitmap); } @Override public String logBitmap(int width, int height, Bitmap.Config config) { int size = Util.getBitmapByteSize(width, height, config); return getBitmapString(size); } @Override public int getSize(Bitmap bitmap) { return Util.getBitmapByteSize(bitmap); } @Override public String toString() { return "SizeStrategy:\n " + groupedMap + "\n" + " SortedSizes" + sortedSizes; } private static String getBitmapString(Bitmap bitmap) { int size = Util.getBitmapByteSize(bitmap); return getBitmapString(size); } @Synthetic static String getBitmapString(int size) { return "[" + size + "]"; } // Non-final for mocking. @VisibleForTesting static class KeyPool extends BaseKeyPool { public Key get(int size) { Key result = super.get(); result.init(size); return result; } @Override protected Key create() { return new Key(this); } } @VisibleForTesting static final class Key implements Poolable { private final KeyPool pool; @Synthetic int size; Key(KeyPool pool) { this.pool = pool; } public void init(int size) { this.size = size; } @Override public boolean equals(Object o) { if (o instanceof Key) { Key other = (Key) o; return size == other.size; } return false; } @Override public int hashCode() { return size; } // PMD.AccessorMethodGeneration: https://github.com/pmd/pmd/issues/807 @SuppressWarnings("PMD.AccessorMethodGeneration") @Override public String toString() { return getBitmapString(size); } @Override public void offer() { pool.offer(this); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/DiskCache.java ================================================ package com.bumptech.glide.load.engine.cache; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import java.io.File; /** An interface for writing to and reading from a disk cache. */ public interface DiskCache { /** An interface for lazily creating a disk cache. */ interface Factory { /** 250 MB of cache. */ int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024; String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache"; /** Returns a new disk cache, or {@code null} if no disk cache could be created. */ @Nullable DiskCache build(); } /** An interface to actually write data to a key in the disk cache. */ interface Writer { /** * Writes data to the file and returns true if the write was successful and should be committed, * and false if the write should be aborted. * * @param file The File the Writer should write to. */ boolean write(@NonNull File file); } /** * Get the cache for the value at the given key. * *

    Note - This is potentially dangerous, someone may write a new value to the file at any point * in time and we won't know about it. * * @param key The key in the cache. * @return An InputStream representing the data at key at the time get is called. */ @Nullable File get(Key key); /** * Write to a key in the cache. {@link Writer} is used so that the cache implementation can * perform actions after the write finishes, like commit (via atomic file rename). * * @param key The key to write to. * @param writer An interface that will write data given an OutputStream for the key. */ void put(Key key, Writer writer); /** * Remove the key and value from the cache. * * @param key The key to remove. */ // Public API. @SuppressWarnings("unused") void delete(Key key); /** Clear the cache. */ void clear(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/DiskCacheAdapter.java ================================================ package com.bumptech.glide.load.engine.cache; import com.bumptech.glide.load.Key; import java.io.File; /** A simple class that returns null for all gets and ignores all writes. */ public class DiskCacheAdapter implements DiskCache { @Override public File get(Key key) { // no op, default for overriders return null; } @Override public void put(Key key, Writer writer) { // no op, default for overriders } @Override public void delete(Key key) { // no op, default for overriders } @Override public void clear() { // no op, default for overriders } /** Default factory for {@link DiskCacheAdapter}. */ public static final class Factory implements DiskCache.Factory { @Override public DiskCache build() { return new DiskCacheAdapter(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/DiskCacheWriteLocker.java ================================================ package com.bumptech.glide.load.engine.cache; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.util.ArrayDeque; import java.util.HashMap; import java.util.Map; import java.util.Queue; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Keeps a map of keys to locks that allows locks to be removed from the map when no longer in use * so the size of the collection is bounded. * *

    This class will be accessed by multiple threads in a thread pool and ensures that the number * of threads interested in each lock is updated atomically so that when the count reaches 0, the * lock can safely be removed from the map. */ final class DiskCacheWriteLocker { private final Map locks = new HashMap<>(); private final WriteLockPool writeLockPool = new WriteLockPool(); void acquire(String safeKey) { WriteLock writeLock; synchronized (this) { writeLock = locks.get(safeKey); if (writeLock == null) { writeLock = writeLockPool.obtain(); locks.put(safeKey, writeLock); } writeLock.interestedThreads++; } writeLock.lock.lock(); } void release(String safeKey) { WriteLock writeLock; synchronized (this) { writeLock = Preconditions.checkNotNull(locks.get(safeKey)); if (writeLock.interestedThreads < 1) { throw new IllegalStateException( "Cannot release a lock that is not held" + ", safeKey: " + safeKey + ", interestedThreads: " + writeLock.interestedThreads); } writeLock.interestedThreads--; if (writeLock.interestedThreads == 0) { WriteLock removed = locks.remove(safeKey); if (!removed.equals(writeLock)) { throw new IllegalStateException( "Removed the wrong lock" + ", expected to remove: " + writeLock + ", but actually removed: " + removed + ", safeKey: " + safeKey); } writeLockPool.offer(removed); } } writeLock.lock.unlock(); } private static class WriteLock { final Lock lock = new ReentrantLock(); int interestedThreads; @Synthetic WriteLock() {} } private static class WriteLockPool { private static final int MAX_POOL_SIZE = 10; private final Queue pool = new ArrayDeque<>(); @Synthetic WriteLockPool() {} WriteLock obtain() { WriteLock result; synchronized (pool) { result = pool.poll(); } if (result == null) { result = new WriteLock(); } return result; } void offer(WriteLock writeLock) { synchronized (pool) { if (pool.size() < MAX_POOL_SIZE) { pool.offer(writeLock); } } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/DiskLruCacheFactory.java ================================================ package com.bumptech.glide.load.engine.cache; import java.io.File; /** * Creates an {@link com.bumptech.glide.disklrucache.DiskLruCache} based disk cache in the specified * disk cache directory. * *

    If you need to make I/O access before returning the cache directory use the {@link * DiskLruCacheFactory#DiskLruCacheFactory(CacheDirectoryGetter, long)} constructor variant. */ // Public API. @SuppressWarnings("unused") public class DiskLruCacheFactory implements DiskCache.Factory { private final long diskCacheSize; private final CacheDirectoryGetter cacheDirectoryGetter; /** Interface called out of UI thread to get the cache folder. */ public interface CacheDirectoryGetter { File getCacheDirectory(); } public DiskLruCacheFactory(final String diskCacheFolder, long diskCacheSize) { this( new CacheDirectoryGetter() { @Override public File getCacheDirectory() { return new File(diskCacheFolder); } }, diskCacheSize); } public DiskLruCacheFactory( final String diskCacheFolder, final String diskCacheName, long diskCacheSize) { this( new CacheDirectoryGetter() { @Override public File getCacheDirectory() { return new File(diskCacheFolder, diskCacheName); } }, diskCacheSize); } /** * When using this constructor {@link CacheDirectoryGetter#getCacheDirectory()} will be called out * of UI thread, allowing to do I/O access without performance impacts. * * @param cacheDirectoryGetter Interface called out of UI thread to get the cache folder. * @param diskCacheSize Desired max bytes size for the LRU disk cache. */ // Public API. @SuppressWarnings("WeakerAccess") public DiskLruCacheFactory(CacheDirectoryGetter cacheDirectoryGetter, long diskCacheSize) { this.diskCacheSize = diskCacheSize; this.cacheDirectoryGetter = cacheDirectoryGetter; } @Override public DiskCache build() { File cacheDir = cacheDirectoryGetter.getCacheDirectory(); if (cacheDir == null) { return null; } if (cacheDir.isDirectory() || cacheDir.mkdirs()) { return DiskLruCacheWrapper.create(cacheDir, diskCacheSize); } return null; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/DiskLruCacheWrapper.java ================================================ /* * Copyright (c) 2013. Bump Technologies Inc. All Rights Reserved. */ package com.bumptech.glide.load.engine.cache; import android.util.Log; import com.bumptech.glide.disklrucache.DiskLruCache; import com.bumptech.glide.disklrucache.DiskLruCache.Value; import com.bumptech.glide.load.Key; import java.io.File; import java.io.IOException; /** * The default DiskCache implementation. There must be no more than one active instance for a given * directory at a time. * * @see #get(java.io.File, long) */ public class DiskLruCacheWrapper implements DiskCache { private static final String TAG = "DiskLruCacheWrapper"; private static final int APP_VERSION = 1; private static final int VALUE_COUNT = 1; private static DiskLruCacheWrapper wrapper; private final SafeKeyGenerator safeKeyGenerator; private final File directory; private final long maxSize; private final DiskCacheWriteLocker writeLocker = new DiskCacheWriteLocker(); private DiskLruCache diskLruCache; /** * Get a DiskCache in the given directory and size. If a disk cache has already been created with * a different directory and/or size, it will be returned instead and the new arguments will be * ignored. * * @param directory The directory for the disk cache * @param maxSize The max size for the disk cache * @return The new disk cache with the given arguments, or the current cache if one already exists * @deprecated Use {@link #create(File, long)} to create a new cache with the specified arguments. */ @SuppressWarnings("deprecation") @Deprecated public static synchronized DiskCache get(File directory, long maxSize) { // TODO calling twice with different arguments makes it return the cache for the same // directory, it's public! if (wrapper == null) { wrapper = new DiskLruCacheWrapper(directory, maxSize); } return wrapper; } /** * Create a new DiskCache in the given directory with a specified max size. * * @param directory The directory for the disk cache * @param maxSize The max size for the disk cache * @return The new disk cache with the given arguments */ @SuppressWarnings("deprecation") public static DiskCache create(File directory, long maxSize) { return new DiskLruCacheWrapper(directory, maxSize); } /** * @deprecated Do not extend this class. */ @Deprecated // Deprecated public API. @SuppressWarnings({"WeakerAccess", "DeprecatedIsStillUsed"}) protected DiskLruCacheWrapper(File directory, long maxSize) { this.directory = directory; this.maxSize = maxSize; this.safeKeyGenerator = new SafeKeyGenerator(); } private synchronized DiskLruCache getDiskCache() throws IOException { if (diskLruCache == null) { diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize); } return diskLruCache; } @Override public File get(Key key) { String safeKey = safeKeyGenerator.getSafeKey(key); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Get: Obtained: " + safeKey + " for for Key: " + key); } File result = null; try { // It is possible that the there will be a put in between these two gets. If so that shouldn't // be a problem because we will always put the same value at the same key so our input streams // will still represent the same data. final DiskLruCache.Value value = getDiskCache().get(safeKey); if (value != null) { result = value.getFile(0); } } catch (IOException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Unable to get from disk cache", e); } } return result; } @Override public void put(Key key, Writer writer) { // We want to make sure that puts block so that data is available when put completes. We may // actually not write any data if we find that data is written by the time we acquire the lock. String safeKey = safeKeyGenerator.getSafeKey(key); writeLocker.acquire(safeKey); try { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Put: Obtained: " + safeKey + " for for Key: " + key); } try { // We assume we only need to put once, so if data was written while we were trying to get // the lock, we can simply abort. DiskLruCache diskCache = getDiskCache(); Value current = diskCache.get(safeKey); if (current != null) { return; } DiskLruCache.Editor editor = diskCache.edit(safeKey); if (editor == null) { throw new IllegalStateException("Had two simultaneous puts for: " + safeKey); } try { File file = editor.getFile(0); if (writer.write(file)) { editor.commit(); } } finally { editor.abortUnlessCommitted(); } } catch (IOException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Unable to put to disk cache", e); } } } finally { writeLocker.release(safeKey); } } @Override public void delete(Key key) { String safeKey = safeKeyGenerator.getSafeKey(key); try { getDiskCache().remove(safeKey); } catch (IOException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Unable to delete from disk cache", e); } } } @Override public synchronized void clear() { try { getDiskCache().delete(); } catch (IOException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Unable to clear disk cache or disk cache cleared externally", e); } } finally { // Delete can close the cache but still throw. If we don't null out the disk cache here, every // subsequent request will try to act on a closed disk cache and fail. By nulling out the disk // cache we at least allow for attempts to open the cache in the future. See #2465. resetDiskCache(); } } private synchronized void resetDiskCache() { diskLruCache = null; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/ExternalCacheDiskCacheFactory.java ================================================ package com.bumptech.glide.load.engine.cache; import android.content.Context; import java.io.File; /** * Creates an {@link com.bumptech.glide.disklrucache.DiskLruCache} based disk cache in the external * disk cache directory. * *

    Images can be read by everyone when using external disk cache. * * @deprecated use {@link ExternalPreferredCacheDiskCacheFactory} instead. */ // Public API. @SuppressWarnings({"unused", "WeakerAccess"}) @Deprecated public final class ExternalCacheDiskCacheFactory extends DiskLruCacheFactory { public ExternalCacheDiskCacheFactory(Context context) { this( context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE); } public ExternalCacheDiskCacheFactory(Context context, int diskCacheSize) { this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize); } public ExternalCacheDiskCacheFactory( final Context context, final String diskCacheName, int diskCacheSize) { super( new CacheDirectoryGetter() { @Override public File getCacheDirectory() { File cacheDirectory = context.getExternalCacheDir(); if (cacheDirectory == null) { return null; } if (diskCacheName != null) { return new File(cacheDirectory, diskCacheName); } return cacheDirectory; } }, diskCacheSize); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/ExternalPreferredCacheDiskCacheFactory.java ================================================ package com.bumptech.glide.load.engine.cache; import android.content.Context; import androidx.annotation.Nullable; import java.io.File; /** * Creates an {@link com.bumptech.glide.disklrucache.DiskLruCache} based disk cache in the external * disk cache directory, which falls back to the internal disk cache if no external storage is * available. If ever fell back to the internal disk cache, will use that one from that moment on. * *

    Images can be read by everyone when using external disk cache. */ // Public API. @SuppressWarnings({"unused", "WeakerAccess"}) public final class ExternalPreferredCacheDiskCacheFactory extends DiskLruCacheFactory { public ExternalPreferredCacheDiskCacheFactory(Context context) { this( context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE); } public ExternalPreferredCacheDiskCacheFactory(Context context, long diskCacheSize) { this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize); } public ExternalPreferredCacheDiskCacheFactory( final Context context, final String diskCacheName, final long diskCacheSize) { super( new CacheDirectoryGetter() { @Nullable private File getInternalCacheDirectory() { File cacheDirectory = context.getCacheDir(); if (cacheDirectory == null) { return null; } if (diskCacheName != null) { return new File(cacheDirectory, diskCacheName); } return cacheDirectory; } @Override public File getCacheDirectory() { File internalCacheDirectory = getInternalCacheDirectory(); // Already used internal cache, so keep using that one, // thus avoiding using both external and internal with transient errors. if (internalCacheDirectory != null && internalCacheDirectory.exists()) { return internalCacheDirectory; } File cacheDirectory = context.getExternalCacheDir(); // Shared storage is not available. if (cacheDirectory == null || !cacheDirectory.canWrite()) { return internalCacheDirectory; } if (diskCacheName != null) { return new File(cacheDirectory, diskCacheName); } return cacheDirectory; } }, diskCacheSize); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/InternalCacheDiskCacheFactory.java ================================================ package com.bumptech.glide.load.engine.cache; import android.content.Context; import java.io.File; /** * Creates an {@link com.bumptech.glide.disklrucache.DiskLruCache} based disk cache in the internal * disk cache directory. */ // Public API. @SuppressWarnings({"WeakerAccess", "unused"}) public final class InternalCacheDiskCacheFactory extends DiskLruCacheFactory { public InternalCacheDiskCacheFactory(Context context) { this( context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE); } public InternalCacheDiskCacheFactory(Context context, long diskCacheSize) { this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize); } public InternalCacheDiskCacheFactory( final Context context, final String diskCacheName, long diskCacheSize) { super( new CacheDirectoryGetter() { @Override public File getCacheDirectory() { File cacheDirectory = context.getCacheDir(); if (cacheDirectory == null) { return null; } if (diskCacheName != null) { return new File(cacheDirectory, diskCacheName); } return cacheDirectory; } }, diskCacheSize); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/LruResourceCache.java ================================================ package com.bumptech.glide.load.engine.cache; import android.annotation.SuppressLint; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.LruCache; /** An LRU in memory cache for {@link com.bumptech.glide.load.engine.Resource}s. */ public class LruResourceCache extends LruCache> implements MemoryCache { private ResourceRemovedListener listener; /** * Constructor for LruResourceCache. * * @param size The maximum size in bytes the in memory cache can use. */ public LruResourceCache(long size) { super(size); } @Override public void setResourceRemovedListener(@NonNull ResourceRemovedListener listener) { this.listener = listener; } @Override protected void onItemEvicted(@NonNull Key key, @Nullable Resource item) { if (listener != null && item != null) { listener.onResourceRemoved(item); } } @Override protected int getSize(@Nullable Resource item) { if (item == null) { return super.getSize(null); } else { return item.getSize(); } } @SuppressLint("InlinedApi") @Override public void trimMemory(int level) { if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { // Entering list of cached background apps // Evict our entire bitmap cache clearMemory(); } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { // The app's UI is no longer visible, or app is in the foreground but system is running // critically low on memory // Evict oldest half of our bitmap cache trimToSize(getMaxSize() / 2); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/MemoryCache.java ================================================ package com.bumptech.glide.load.engine.cache; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.Resource; /** An interface for adding and removing resources from an in memory cache. */ public interface MemoryCache { /** An interface that will be called whenever a bitmap is removed from the cache. */ interface ResourceRemovedListener { void onResourceRemoved(@NonNull Resource removed); } /** Returns the sum of the sizes of all the contents of the cache in bytes. */ long getCurrentSize(); /** Returns the current maximum size in bytes of the cache. */ long getMaxSize(); /** * Adjust the maximum size of the cache by multiplying the original size of the cache by the given * multiplier. * *

    If the size multiplier causes the size of the cache to be decreased, items will be evicted * until the cache is smaller than the new size. * * @param multiplier A size multiplier {@code >= 0}. */ void setSizeMultiplier(float multiplier); /** * Removes the value for the given key and returns it if present or null otherwise. * * @param key The key. */ @Nullable Resource remove(@NonNull Key key); /** * Add bitmap to the cache with the given key. * * @param key The key to retrieve the bitmap. * @param resource The {@link com.bumptech.glide.load.engine.EngineResource} to store. * @return The old value of key (null if key is not in map). */ @Nullable Resource put(@NonNull Key key, @Nullable Resource resource); /** * Set the listener to be called when a bitmap is removed from the cache. * * @param listener The listener. */ void setResourceRemovedListener(@NonNull ResourceRemovedListener listener); /** Evict all items from the memory cache. */ void clearMemory(); /** * Trim the memory cache to the appropriate level. Typically called on the callback onTrimMemory. * * @param level This integer represents a trim level as specified in {@link * android.content.ComponentCallbacks2}. */ void trimMemory(int level); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/MemoryCacheAdapter.java ================================================ package com.bumptech.glide.load.engine.cache; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.Resource; /** A simple class that ignores all puts and returns null for all gets. */ public class MemoryCacheAdapter implements MemoryCache { private ResourceRemovedListener listener; @Override public long getCurrentSize() { return 0; } @Override public long getMaxSize() { return 0; } @Override public void setSizeMultiplier(float multiplier) { // Do nothing. } @Nullable @Override public Resource remove(@NonNull Key key) { return null; } @Nullable @Override public Resource put(@NonNull Key key, @Nullable Resource resource) { if (resource != null) { listener.onResourceRemoved(resource); } return null; } @Override public void setResourceRemovedListener(@NonNull ResourceRemovedListener listener) { this.listener = listener; } @Override public void clearMemory() { // Do nothing. } @Override public void trimMemory(int level) { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/MemorySizeCalculator.java ================================================ package com.bumptech.glide.load.engine.cache; import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.Context; import android.os.Build; import android.text.format.Formatter; import android.util.DisplayMetrics; import android.util.Log; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; /** * A calculator that tries to intelligently determine cache sizes for a given device based on some * constants and the devices screen density, width, and height. */ public final class MemorySizeCalculator { private static final String TAG = "MemorySizeCalculator"; @VisibleForTesting static final int BYTES_PER_ARGB_8888_PIXEL = 4; private static final int LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR = 2; private final int bitmapPoolSize; private final int memoryCacheSize; private final Context context; private final int arrayPoolSize; interface ScreenDimensions { int getWidthPixels(); int getHeightPixels(); } // Package private to avoid PMD warning. MemorySizeCalculator(MemorySizeCalculator.Builder builder) { this.context = builder.context; arrayPoolSize = isLowMemoryDevice(builder.activityManager) ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR : builder.arrayPoolSizeBytes; int maxSize = getMaxSize( builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier); int widthPixels = builder.screenDimensions.getWidthPixels(); int heightPixels = builder.screenDimensions.getHeightPixels(); int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL; int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens); int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens); int availableSize = maxSize - arrayPoolSize; if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) { memoryCacheSize = targetMemoryCacheSize; bitmapPoolSize = targetBitmapPoolSize; } else { float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens); memoryCacheSize = Math.round(part * builder.memoryCacheScreens); bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens); } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Calculation complete" + ", Calculated memory cache size: " + toMb(memoryCacheSize) + ", pool size: " + toMb(bitmapPoolSize) + ", byte array size: " + toMb(arrayPoolSize) + ", memory class limited? " + (targetMemoryCacheSize + targetBitmapPoolSize > maxSize) + ", max size: " + toMb(maxSize) + ", memoryClass: " + builder.activityManager.getMemoryClass() + ", isLowMemoryDevice: " + isLowMemoryDevice(builder.activityManager)); } } /** Returns the recommended memory cache size for the device it is run on in bytes. */ public int getMemoryCacheSize() { return memoryCacheSize; } /** Returns the recommended bitmap pool size for the device it is run on in bytes. */ public int getBitmapPoolSize() { return bitmapPoolSize; } /** Returns the recommended array pool size for the device it is run on in bytes. */ public int getArrayPoolSizeInBytes() { return arrayPoolSize; } private static int getMaxSize( ActivityManager activityManager, float maxSizeMultiplier, float lowMemoryMaxSizeMultiplier) { final int memoryClassBytes = activityManager.getMemoryClass() * 1024 * 1024; final boolean isLowMemoryDevice = isLowMemoryDevice(activityManager); return Math.round( memoryClassBytes * (isLowMemoryDevice ? lowMemoryMaxSizeMultiplier : maxSizeMultiplier)); } private String toMb(int bytes) { return Formatter.formatFileSize(context, bytes); } @TargetApi(Build.VERSION_CODES.KITKAT) @Synthetic static boolean isLowMemoryDevice(ActivityManager activityManager) { // Explicitly check with an if statement, on some devices both parts of boolean expressions // can be evaluated even if we'd normally expect a short circuit. //noinspection SimplifiableIfStatement if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return activityManager.isLowRamDevice(); } else { return true; } } /** * Constructs an {@link MemorySizeCalculator} with reasonable defaults that can be optionally * overridden. */ // Public API. @SuppressWarnings({"WeakerAccess", "unused"}) public static final class Builder { @VisibleForTesting static final int MEMORY_CACHE_TARGET_SCREENS = 2; /** * On Android O+, we use {@link android.graphics.Bitmap.Config#HARDWARE} for all reasonably * sized images unless we're creating thumbnails for the first time. As a result, the Bitmap * pool is much less important on O than it was on previous versions. */ static final int BITMAP_POOL_TARGET_SCREENS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 4 : 1; static final float MAX_SIZE_MULTIPLIER = 0.4f; static final float LOW_MEMORY_MAX_SIZE_MULTIPLIER = 0.33f; // 4MB. static final int ARRAY_POOL_SIZE_BYTES = 4 * 1024 * 1024; @Synthetic final Context context; // Modifiable (non-final) for testing. @Synthetic ActivityManager activityManager; @Synthetic ScreenDimensions screenDimensions; @Synthetic float memoryCacheScreens = MEMORY_CACHE_TARGET_SCREENS; @Synthetic float bitmapPoolScreens = BITMAP_POOL_TARGET_SCREENS; @Synthetic float maxSizeMultiplier = MAX_SIZE_MULTIPLIER; @Synthetic float lowMemoryMaxSizeMultiplier = LOW_MEMORY_MAX_SIZE_MULTIPLIER; @Synthetic int arrayPoolSizeBytes = ARRAY_POOL_SIZE_BYTES; public Builder(Context context) { this.context = context; activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); screenDimensions = new DisplayMetricsScreenDimensions(context.getResources().getDisplayMetrics()); // On Android O+ Bitmaps are allocated natively, ART is much more efficient at managing // garbage and we rely heavily on HARDWARE Bitmaps, making Bitmap re-use much less important. // We prefer to preserve RAM on these devices and take the small performance hit of not // re-using Bitmaps and textures when loading very small images or generating thumbnails. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLowMemoryDevice(activityManager)) { bitmapPoolScreens = 0; } } /** * Sets the number of device screens worth of pixels the {@link * com.bumptech.glide.load.engine.cache.MemoryCache} should be able to hold and returns this * Builder. */ public Builder setMemoryCacheScreens(float memoryCacheScreens) { Preconditions.checkArgument( memoryCacheScreens >= 0, "Memory cache screens must be greater than or equal to 0"); this.memoryCacheScreens = memoryCacheScreens; return this; } /** * Sets the number of device screens worth of pixels the {@link * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} should be able to hold and returns * this Builder. */ public Builder setBitmapPoolScreens(float bitmapPoolScreens) { Preconditions.checkArgument( bitmapPoolScreens >= 0, "Bitmap pool screens must be greater than or equal to 0"); this.bitmapPoolScreens = bitmapPoolScreens; return this; } /** * Sets the maximum percentage of the device's memory class for standard devices that can be * taken up by Glide's {@link com.bumptech.glide.load.engine.cache.MemoryCache} and {@link * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} put together, and returns this * builder. */ public Builder setMaxSizeMultiplier(float maxSizeMultiplier) { Preconditions.checkArgument( maxSizeMultiplier >= 0 && maxSizeMultiplier <= 1, "Size multiplier must be between 0 and 1"); this.maxSizeMultiplier = maxSizeMultiplier; return this; } /** * Sets the maximum percentage of the device's memory class for low ram devices that can be * taken up by Glide's {@link com.bumptech.glide.load.engine.cache.MemoryCache} and {@link * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} put together, and returns this * builder. * * @see ActivityManager#isLowRamDevice() */ public Builder setLowMemoryMaxSizeMultiplier(float lowMemoryMaxSizeMultiplier) { Preconditions.checkArgument( lowMemoryMaxSizeMultiplier >= 0 && lowMemoryMaxSizeMultiplier <= 1, "Low memory max size multiplier must be between 0 and 1"); this.lowMemoryMaxSizeMultiplier = lowMemoryMaxSizeMultiplier; return this; } /** * Sets the size in bytes of the {@link com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool} * to use to store temporary arrays while decoding data and returns this builder. * *

    This number will be halved on low memory devices that return {@code true} from {@link * ActivityManager#isLowRamDevice()}. */ public Builder setArrayPoolSize(int arrayPoolSizeBytes) { this.arrayPoolSizeBytes = arrayPoolSizeBytes; return this; } @VisibleForTesting Builder setActivityManager(ActivityManager activityManager) { this.activityManager = activityManager; return this; } @VisibleForTesting Builder setScreenDimensions(ScreenDimensions screenDimensions) { this.screenDimensions = screenDimensions; return this; } public MemorySizeCalculator build() { return new MemorySizeCalculator(this); } } private static final class DisplayMetricsScreenDimensions implements ScreenDimensions { private final DisplayMetrics displayMetrics; DisplayMetricsScreenDimensions(DisplayMetrics displayMetrics) { this.displayMetrics = displayMetrics; } @Override public int getWidthPixels() { return displayMetrics.widthPixels; } @Override public int getHeightPixels() { return displayMetrics.heightPixels; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/cache/SafeKeyGenerator.java ================================================ package com.bumptech.glide.load.engine.cache; import androidx.annotation.NonNull; import androidx.core.util.Pools; import com.bumptech.glide.load.Key; import com.bumptech.glide.util.LruCache; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import com.bumptech.glide.util.pool.FactoryPools; import com.bumptech.glide.util.pool.StateVerifier; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * A class that generates and caches safe and unique string file names from {@link * com.bumptech.glide.load.Key}s. */ // Public API. @SuppressWarnings("WeakerAccess") public class SafeKeyGenerator { private final LruCache loadIdToSafeHash = new LruCache<>(1000); private final Pools.Pool digestPool = FactoryPools.threadSafe( 10, new FactoryPools.Factory() { @Override public PoolableDigestContainer create() { try { return new PoolableDigestContainer(MessageDigest.getInstance("SHA-256")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }); public String getSafeKey(Key key) { String safeKey; synchronized (loadIdToSafeHash) { safeKey = loadIdToSafeHash.get(key); } if (safeKey == null) { safeKey = calculateHexStringDigest(key); } synchronized (loadIdToSafeHash) { loadIdToSafeHash.put(key, safeKey); } return safeKey; } private String calculateHexStringDigest(Key key) { PoolableDigestContainer container = Preconditions.checkNotNull(digestPool.acquire()); try { key.updateDiskCacheKey(container.messageDigest); // calling digest() will automatically reset() return Util.sha256BytesToHex(container.messageDigest.digest()); } finally { digestPool.release(container); } } private static final class PoolableDigestContainer implements FactoryPools.Poolable { @Synthetic final MessageDigest messageDigest; private final StateVerifier stateVerifier = StateVerifier.newInstance(); PoolableDigestContainer(MessageDigest messageDigest) { this.messageDigest = messageDigest; } @NonNull @Override public StateVerifier getVerifier() { return stateVerifier; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/executor/GlideExecutor.java ================================================ package com.bumptech.glide.load.engine.executor; import android.os.StrictMode; import android.os.StrictMode.ThreadPolicy; import android.text.TextUtils; import android.util.Log; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Synthetic; import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; /** A prioritized {@link ThreadPoolExecutor} for running jobs in Glide. */ public final class GlideExecutor implements ExecutorService { /** * The default thread name prefix for executors used to load/decode/transform data not found in * cache. */ static final String DEFAULT_SOURCE_EXECUTOR_NAME = "source"; /** * The default thread name prefix for executors used to load/decode/transform data found in * Glide's cache. */ static final String DEFAULT_DISK_CACHE_EXECUTOR_NAME = "disk-cache"; /** * The default thread count for executors used to load/decode/transform data found in Glide's * cache. */ static final int DEFAULT_DISK_CACHE_EXECUTOR_THREADS = 1; private static final String TAG = "GlideExecutor"; /** * The default thread name prefix for executors from unlimited thread pool used to * load/decode/transform data not found in cache. */ private static final String DEFAULT_SOURCE_UNLIMITED_EXECUTOR_NAME = "source-unlimited"; static final String DEFAULT_ANIMATION_EXECUTOR_NAME = "animation"; /** The default keep alive time for threads in our cached thread pools in milliseconds. */ private static final long KEEP_ALIVE_TIME_MS = TimeUnit.SECONDS.toMillis(10); // Don't use more than four threads when automatically determining thread count.. private static final int MAXIMUM_AUTOMATIC_THREAD_COUNT = 4; // May be accessed on other threads, but this is an optimization only so it's ok if we set its // value more than once. private static volatile int bestThreadCount; private final ExecutorService delegate; /** The default priority for threads created by Glide. */ public static final int DEFAULT_PRIORITY = android.os.Process.THREAD_PRIORITY_BACKGROUND + android.os.Process.THREAD_PRIORITY_MORE_FAVORABLE; /** * Returns a new {@link Builder} with the {@link #DEFAULT_DISK_CACHE_EXECUTOR_THREADS} threads, * {@link #DEFAULT_DISK_CACHE_EXECUTOR_NAME} name and {@link UncaughtThrowableStrategy#DEFAULT} * uncaught throwable strategy. * *

    Disk cache executors do not allow network operations on their threads. */ public static GlideExecutor.Builder newDiskCacheBuilder() { return new GlideExecutor.Builder(/* preventNetworkOperations= */ true) .setThreadCount(DEFAULT_DISK_CACHE_EXECUTOR_THREADS) .setName(DEFAULT_DISK_CACHE_EXECUTOR_NAME); } /** Shortcut for calling {@link Builder#build()} on {@link #newDiskCacheBuilder()}. */ public static GlideExecutor newDiskCacheExecutor() { return newDiskCacheBuilder().build(); } /** * @deprecated Use {@link #newDiskCacheBuilder()} and {@link * Builder#setUncaughtThrowableStrategy(UncaughtThrowableStrategy)} instead. */ // Public API. @SuppressWarnings("unused") @Deprecated public static GlideExecutor newDiskCacheExecutor( UncaughtThrowableStrategy uncaughtThrowableStrategy) { return newDiskCacheBuilder().setUncaughtThrowableStrategy(uncaughtThrowableStrategy).build(); } /** * @deprecated Use {@link #newDiskCacheBuilder()} instead. */ // Public API. @SuppressWarnings("WeakerAccess") @Deprecated public static GlideExecutor newDiskCacheExecutor( int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) { return newDiskCacheBuilder() .setThreadCount(threadCount) .setName(name) .setUncaughtThrowableStrategy(uncaughtThrowableStrategy) .build(); } /** * Returns a new {@link Builder} with the default thread count returned from {@link * #calculateBestThreadCount()}, the {@link #DEFAULT_SOURCE_EXECUTOR_NAME} thread name prefix, and * the {@link * com.bumptech.glide.load.engine.executor.GlideExecutor.UncaughtThrowableStrategy#DEFAULT} * uncaught throwable strategy. * *

    Source executors allow network operations on their threads. */ public static GlideExecutor.Builder newSourceBuilder() { return new GlideExecutor.Builder(/* preventNetworkOperations= */ false) .setThreadCount(calculateBestThreadCount()) .setName(DEFAULT_SOURCE_EXECUTOR_NAME); } /** Shortcut for calling {@link Builder#build()} on {@link #newSourceBuilder()}. */ public static GlideExecutor newSourceExecutor() { return newSourceBuilder().build(); } /** * @deprecated Use {@link #newSourceBuilder()} instead. */ // Public API. @SuppressWarnings("unused") @Deprecated public static GlideExecutor newSourceExecutor( UncaughtThrowableStrategy uncaughtThrowableStrategy) { return newSourceBuilder().setUncaughtThrowableStrategy(uncaughtThrowableStrategy).build(); } /** * @deprecated Use {@link #newSourceBuilder()} instead. */ // Public API. @SuppressWarnings("WeakerAccess") @Deprecated public static GlideExecutor newSourceExecutor( int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) { return newSourceBuilder() .setThreadCount(threadCount) .setName(name) .setUncaughtThrowableStrategy(uncaughtThrowableStrategy) .build(); } /** * Returns a new unlimited thread pool with zero core thread count to make sure no threads are * created by default, {@link #KEEP_ALIVE_TIME_MS} keep alive time, the {@link * #DEFAULT_SOURCE_UNLIMITED_EXECUTOR_NAME} thread name prefix, the {@link * com.bumptech.glide.load.engine.executor.GlideExecutor.UncaughtThrowableStrategy#DEFAULT} * uncaught throwable strategy, and the {@link SynchronousQueue} since using default unbounded * blocking queue, for example, {@link PriorityBlockingQueue} effectively won't create more than * {@code corePoolSize} threads. See * ThreadPoolExecutor documentation. * *

    Source executors allow network operations on their threads. */ public static GlideExecutor newUnlimitedSourceExecutor() { return new GlideExecutor( new ThreadPoolExecutor( 0, Integer.MAX_VALUE, KEEP_ALIVE_TIME_MS, TimeUnit.MILLISECONDS, new SynchronousQueue(), new DefaultThreadFactory( new DefaultPriorityThreadFactory(), DEFAULT_SOURCE_UNLIMITED_EXECUTOR_NAME, UncaughtThrowableStrategy.DEFAULT, false))); } /** * Returns a new fixed thread pool that defaults to either one or two threads depending on the * number of available cores to use when loading frames of animations. * *

    Animation executors do not allow network operations on their threads. */ public static GlideExecutor.Builder newAnimationBuilder() { int maximumPoolSize = calculateAnimationExecutorThreadCount(); return new GlideExecutor.Builder(/* preventNetworkOperations= */ true) .setThreadCount(maximumPoolSize) .setName(DEFAULT_ANIMATION_EXECUTOR_NAME); } static int calculateAnimationExecutorThreadCount() { int bestThreadCount = calculateBestThreadCount(); // We don't want to add a ton of threads running animations in parallel with our source and // disk cache executors. Doing so adds unnecessary CPU load and can also dramatically increase // our maximum memory usage. Typically one thread is sufficient here, but for higher end devices // with more cores, two threads can provide better performance if lots of GIFs are showing at // once. return bestThreadCount >= 4 ? 2 : 1; } /** Shortcut for calling {@link Builder#build()} on {@link #newAnimationBuilder()}. */ public static GlideExecutor newAnimationExecutor() { return newAnimationBuilder().build(); } /** * @deprecated Use {@link #newAnimationBuilder()} instead. */ // Public API. @SuppressWarnings("WeakerAccess") @Deprecated public static GlideExecutor newAnimationExecutor( int threadCount, UncaughtThrowableStrategy uncaughtThrowableStrategy) { return newAnimationBuilder() .setThreadCount(threadCount) .setUncaughtThrowableStrategy(uncaughtThrowableStrategy) .build(); } @VisibleForTesting GlideExecutor(ExecutorService delegate) { this.delegate = delegate; } @Override public void execute(@NonNull Runnable command) { delegate.execute(command); } @NonNull @Override public Future submit(@NonNull Runnable task) { return delegate.submit(task); } @NonNull @Override public List> invokeAll(@NonNull Collection> tasks) throws InterruptedException { return delegate.invokeAll(tasks); } @NonNull @Override public List> invokeAll( @NonNull Collection> tasks, long timeout, @NonNull TimeUnit unit) throws InterruptedException { return delegate.invokeAll(tasks, timeout, unit); } @NonNull @Override public T invokeAny(@NonNull Collection> tasks) throws InterruptedException, ExecutionException { return delegate.invokeAny(tasks); } @Override public T invokeAny( @NonNull Collection> tasks, long timeout, @NonNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return delegate.invokeAny(tasks, timeout, unit); } @NonNull @Override public Future submit(@NonNull Runnable task, T result) { return delegate.submit(task, result); } @Override public Future submit(@NonNull Callable task) { return delegate.submit(task); } @Override public void shutdown() { delegate.shutdown(); } @NonNull @Override public List shutdownNow() { return delegate.shutdownNow(); } @Override public boolean isShutdown() { return delegate.isShutdown(); } @Override public boolean isTerminated() { return delegate.isTerminated(); } @Override public boolean awaitTermination(long timeout, @NonNull TimeUnit unit) throws InterruptedException { return delegate.awaitTermination(timeout, unit); } @Override public String toString() { return delegate.toString(); } /** Determines the number of cores available on the device. */ // Public API. @SuppressWarnings("WeakerAccess") public static int calculateBestThreadCount() { if (bestThreadCount == 0) { bestThreadCount = Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors()); } return bestThreadCount; } /** * A strategy for handling unexpected and uncaught {@link Throwable}s thrown by futures run on the * pool. */ public interface UncaughtThrowableStrategy { /** Silently catches and ignores the uncaught {@link Throwable}s. */ // Public API. @SuppressWarnings("unused") UncaughtThrowableStrategy IGNORE = new UncaughtThrowableStrategy() { @Override public void handle(Throwable t) { // ignore } }; /** Logs the uncaught {@link Throwable}s using {@link #TAG} and {@link Log}. */ UncaughtThrowableStrategy LOG = new UncaughtThrowableStrategy() { @Override public void handle(Throwable t) { if (t != null && Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Request threw uncaught throwable", t); } } }; /** Rethrows the uncaught {@link Throwable}s to crash the app. */ // Public API. @SuppressWarnings("unused") UncaughtThrowableStrategy THROW = new UncaughtThrowableStrategy() { @Override public void handle(Throwable t) { if (t != null) { throw new RuntimeException("Request threw uncaught throwable", t); } } }; /** The default strategy, currently {@link #LOG}. */ UncaughtThrowableStrategy DEFAULT = LOG; void handle(Throwable t); } private static final class DefaultPriorityThreadFactory implements ThreadFactory { @Override public Thread newThread(@NonNull Runnable runnable) { return new Thread(runnable) { @Override public void run() { // why PMD suppression is needed: https://github.com/pmd/pmd/issues/808 android.os.Process.setThreadPriority(DEFAULT_PRIORITY); // NOPMD AccessorMethodGeneration super.run(); } }; } } /** * A {@link java.util.concurrent.ThreadFactory} that builds threads slightly above priority {@link * android.os.Process#THREAD_PRIORITY_BACKGROUND}. */ private static final class DefaultThreadFactory implements ThreadFactory { private final ThreadFactory delegate; private final String name; @Synthetic final UncaughtThrowableStrategy uncaughtThrowableStrategy; @Synthetic final boolean preventNetworkOperations; private final AtomicInteger threadNum = new AtomicInteger(); DefaultThreadFactory( ThreadFactory delegate, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy, boolean preventNetworkOperations) { this.delegate = delegate; this.name = name; this.uncaughtThrowableStrategy = uncaughtThrowableStrategy; this.preventNetworkOperations = preventNetworkOperations; } @Override public Thread newThread(@NonNull final Runnable runnable) { Thread newThread = delegate.newThread( new Runnable() { @Override public void run() { if (preventNetworkOperations) { StrictMode.setThreadPolicy( new ThreadPolicy.Builder().detectNetwork().penaltyDeath().build()); } try { runnable.run(); } catch (Throwable t) { uncaughtThrowableStrategy.handle(t); } } }); newThread.setName("glide-" + name + "-thread-" + threadNum.getAndIncrement()); return newThread; } } /** A builder for {@link GlideExecutor}s. */ public static final class Builder { /** * Prevents core and non-core threads from timing out ever if provided to {@link * #setThreadTimeoutMillis(long)}. */ public static final long NO_THREAD_TIMEOUT = 0L; private final boolean preventNetworkOperations; private int corePoolSize; private int maximumPoolSize; @NonNull private ThreadFactory threadFactory = new DefaultPriorityThreadFactory(); @NonNull private UncaughtThrowableStrategy uncaughtThrowableStrategy = UncaughtThrowableStrategy.DEFAULT; private String name; private long threadTimeoutMillis; private Function onExecuteDecorator; @Synthetic Builder(boolean preventNetworkOperations) { this.preventNetworkOperations = preventNetworkOperations; } /** * Allows both core and non-core threads in the executor to be terminated if no tasks arrive for * at least the given timeout milliseconds. * *

    Use {@link #NO_THREAD_TIMEOUT} to remove a previously set timeout. */ public Builder setThreadTimeoutMillis(long threadTimeoutMillis) { this.threadTimeoutMillis = threadTimeoutMillis; return this; } /** Sets the maximum number of threads to use. */ public Builder setThreadCount(@IntRange(from = 1) int threadCount) { corePoolSize = threadCount; maximumPoolSize = threadCount; return this; } /** * Sets the {@link ThreadFactory} responsible for creating threads and setting their priority. * *

    Usage of this method may override other options on this builder. No guarantees are * provided with regards to the behavior of this method or how it interacts with other methods * on the builder. Use at your own risk. * * @deprecated This is an experimental method that may be removed without warning in a future * version. */ @Deprecated public Builder setThreadFactory(@NonNull ThreadFactory threadFactory) { this.threadFactory = threadFactory; return this; } /** * Sets the {@link UncaughtThrowableStrategy} to use for unexpected exceptions thrown by tasks * on {@link GlideExecutor}s built by this {@code Builder}. */ public Builder setUncaughtThrowableStrategy(@NonNull UncaughtThrowableStrategy strategy) { this.uncaughtThrowableStrategy = strategy; return this; } /** * Sets the prefix to use for each thread name created by any {@link GlideExecutor}s built by * this {@code Builder}. */ public Builder setName(String name) { this.name = name; return this; } /** * Sets the decorator to be applied to each runnable executed by the executor. * *

    This is an experimental method that may be removed without warning in a future version. */ public Builder experimentalSetOnExecuteDecorator( Function onExecuteDecorator) { this.onExecuteDecorator = onExecuteDecorator; return this; } /** Builds a new {@link GlideExecutor} with any previously specified options. */ public GlideExecutor build() { if (TextUtils.isEmpty(name)) { throw new IllegalArgumentException( "Name must be non-null and non-empty, but given: " + name); } ThreadFactory factory = new DefaultThreadFactory( threadFactory, name, uncaughtThrowableStrategy, preventNetworkOperations); ThreadPoolExecutor executor; if (onExecuteDecorator != null) { executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, /* keepAliveTime= */ threadTimeoutMillis, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<>(), factory) { @Override public void execute(@NonNull Runnable command) { super.execute(onExecuteDecorator.apply(command)); } }; } else { executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, /* keepAliveTime= */ threadTimeoutMillis, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<>(), factory); } if (threadTimeoutMillis != NO_THREAD_TIMEOUT) { executor.allowCoreThreadTimeOut(true); } return new GlideExecutor(executor); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/executor/RuntimeCompat.java ================================================ package com.bumptech.glide.load.engine.executor; import android.os.Build; import android.os.StrictMode; import android.os.StrictMode.ThreadPolicy; import android.util.Log; import java.io.File; import java.io.FilenameFilter; import java.util.regex.Pattern; /** Compatibility methods for {@link java.lang.Runtime}. */ final class RuntimeCompat { private static final String TAG = "GlideRuntimeCompat"; private static final String CPU_NAME_REGEX = "cpu[0-9]+"; private static final String CPU_LOCATION = "/sys/devices/system/cpu/"; private RuntimeCompat() { // Utility class. } /** Determines the number of cores available on the device. */ static int availableProcessors() { int cpus = Runtime.getRuntime().availableProcessors(); if (Build.VERSION.SDK_INT < 17) { cpus = Math.max(getCoreCountPre17(), cpus); } return cpus; } /** * Determines the number of cores available on the device (pre-v17). * *

    Before Jellybean, {@link Runtime#availableProcessors()} returned the number of awake cores, * which may not be the number of available cores depending on the device's current state. See * https://stackoverflow.com/a/30150409. * * @return the maximum number of processors available to the VM; never smaller than one */ @SuppressWarnings("PMD") private static int getCoreCountPre17() { // We override the current ThreadPolicy to allow disk reads. // This shouldn't actually do disk-IO and accesses a device file. // See: https://github.com/bumptech/glide/issues/1170 File[] cpus = null; ThreadPolicy originalPolicy = StrictMode.allowThreadDiskReads(); try { File cpuInfo = new File(CPU_LOCATION); final Pattern cpuNamePattern = Pattern.compile(CPU_NAME_REGEX); cpus = cpuInfo.listFiles( new FilenameFilter() { @Override public boolean accept(File file, String s) { return cpuNamePattern.matcher(s).matches(); } }); } catch (Throwable t) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Failed to calculate accurate cpu count", t); } } finally { StrictMode.setThreadPolicy(originalPolicy); } return Math.max(1, cpus != null ? cpus.length : 0); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java ================================================ package com.bumptech.glide.load.engine.prefill; import android.graphics.Bitmap; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.load.resource.bitmap.BitmapResource; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.security.MessageDigest; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; /** * A class that allocates {@link android.graphics.Bitmap Bitmaps} to make sure that the {@link * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} is pre-populated. * *

    By posting to the main thread with backoffs, we try to avoid ANRs when the garbage collector * gets into a state where a high percentage of {@link Bitmap} allocations trigger a stop the world * GC. We try to detect whether or not a GC has occurred by only allowing our allocator to run for a * limited number of milliseconds. Since the allocations themselves very fast, a GC is the most * likely reason for a substantial delay. If we detect our allocator has run for more than our * limit, we assume a GC has occurred, stop the current allocations, and try again after a delay. */ final class BitmapPreFillRunner implements Runnable { @VisibleForTesting static final String TAG = "PreFillRunner"; private static final Clock DEFAULT_CLOCK = new Clock(); /** * The maximum number of millis we can run before posting. Set to match and detect the duration of * non concurrent GCs. */ static final long MAX_DURATION_MS = 32; /** * The amount of time in ms we wait before continuing to allocate after the first GC is detected. */ static final long INITIAL_BACKOFF_MS = 40; /** The amount by which the current backoff time is multiplied each time we detect a GC. */ static final int BACKOFF_RATIO = 4; /** The maximum amount of time in ms we wait before continuing to allocate. */ static final long MAX_BACKOFF_MS = TimeUnit.SECONDS.toMillis(1); private final BitmapPool bitmapPool; private final MemoryCache memoryCache; private final PreFillQueue toPrefill; private final Clock clock; private final Set seenTypes = new HashSet<>(); private final Handler handler; private long currentDelay = INITIAL_BACKOFF_MS; private boolean isCancelled; // Public API. @SuppressWarnings("WeakerAccess") public BitmapPreFillRunner( BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder) { this( bitmapPool, memoryCache, allocationOrder, DEFAULT_CLOCK, new Handler(Looper.getMainLooper())); } @VisibleForTesting BitmapPreFillRunner( BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder, Clock clock, Handler handler) { this.bitmapPool = bitmapPool; this.memoryCache = memoryCache; this.toPrefill = allocationOrder; this.clock = clock; this.handler = handler; } public void cancel() { isCancelled = true; } /** * Attempts to allocate {@link android.graphics.Bitmap}s and returns {@code true} if there are * more {@link android.graphics.Bitmap}s to allocate and {@code false} otherwise. */ @VisibleForTesting boolean allocate() { long start = clock.now(); while (!toPrefill.isEmpty() && !isGcDetected(start)) { PreFillType toAllocate = toPrefill.remove(); final Bitmap bitmap; if (!seenTypes.contains(toAllocate)) { seenTypes.add(toAllocate); bitmap = bitmapPool.getDirty( toAllocate.getWidth(), toAllocate.getHeight(), toAllocate.getConfig()); } else { bitmap = Bitmap.createBitmap( toAllocate.getWidth(), toAllocate.getHeight(), toAllocate.getConfig()); } // Order matters here! If the Bitmap is too large or the BitmapPool is too full, it may be // recycled after the call to bitmapPool#put below. int bitmapSize = Util.getBitmapByteSize(bitmap); // Don't over fill the memory cache to avoid evicting useful resources, but make sure it's // not empty so that we use all available space. if (getFreeMemoryCacheBytes() >= bitmapSize) { // We could probably make UniqueKey just always return false from equals, // but the allocation of the Key is not nearly as expensive as the allocation of the Bitmap, // so it's probably not worth it. @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") Key uniqueKey = new UniqueKey(); memoryCache.put(uniqueKey, BitmapResource.obtain(bitmap, bitmapPool)); } else { bitmapPool.put(bitmap); } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "allocated [" + toAllocate.getWidth() + "x" + toAllocate.getHeight() + "] " + toAllocate.getConfig() + " size: " + bitmapSize); } } return !isCancelled && !toPrefill.isEmpty(); } private boolean isGcDetected(long startTimeMs) { return clock.now() - startTimeMs >= MAX_DURATION_MS; } private long getFreeMemoryCacheBytes() { return memoryCache.getMaxSize() - memoryCache.getCurrentSize(); } @Override public void run() { if (allocate()) { handler.postDelayed(this, getNextDelay()); } } private long getNextDelay() { long result = currentDelay; currentDelay = Math.min(currentDelay * BACKOFF_RATIO, MAX_BACKOFF_MS); return result; } private static final class UniqueKey implements Key { @Synthetic @SuppressWarnings("WeakerAccess") UniqueKey() {} @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { throw new UnsupportedOperationException(); } } @VisibleForTesting static class Clock { long now() { return SystemClock.currentThreadTimeMillis(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFiller.java ================================================ package com.bumptech.glide.load.engine.prefill; import android.graphics.Bitmap; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.util.Util; import java.util.HashMap; import java.util.Map; /** * A class for pre-filling {@link android.graphics.Bitmap Bitmaps} in a {@link * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool}. */ public final class BitmapPreFiller { private final MemoryCache memoryCache; private final BitmapPool bitmapPool; private final DecodeFormat defaultFormat; private BitmapPreFillRunner current; public BitmapPreFiller( MemoryCache memoryCache, BitmapPool bitmapPool, DecodeFormat defaultFormat) { this.memoryCache = memoryCache; this.bitmapPool = bitmapPool; this.defaultFormat = defaultFormat; } @SuppressWarnings("deprecation") public void preFill(PreFillType.Builder... bitmapAttributeBuilders) { if (current != null) { current.cancel(); } PreFillType[] bitmapAttributes = new PreFillType[bitmapAttributeBuilders.length]; for (int i = 0; i < bitmapAttributeBuilders.length; i++) { PreFillType.Builder builder = bitmapAttributeBuilders[i]; if (builder.getConfig() == null) { builder.setConfig( defaultFormat == DecodeFormat.PREFER_ARGB_8888 ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); } bitmapAttributes[i] = builder.build(); } PreFillQueue allocationOrder = generateAllocationOrder(bitmapAttributes); current = new BitmapPreFillRunner(bitmapPool, memoryCache, allocationOrder); Util.postOnUiThread(current); } @VisibleForTesting PreFillQueue generateAllocationOrder(PreFillType... preFillSizes) { final long maxSize = memoryCache.getMaxSize() - memoryCache.getCurrentSize() + bitmapPool.getMaxSize(); int totalWeight = 0; for (PreFillType size : preFillSizes) { totalWeight += size.getWeight(); } final float bytesPerWeight = maxSize / (float) totalWeight; Map attributeToCount = new HashMap<>(); for (PreFillType size : preFillSizes) { int bytesForSize = Math.round(bytesPerWeight * size.getWeight()); int bytesPerBitmap = getSizeInBytes(size); int bitmapsForSize = bytesForSize / bytesPerBitmap; attributeToCount.put(size, bitmapsForSize); } return new PreFillQueue(attributeToCount); } private static int getSizeInBytes(PreFillType size) { return Util.getBitmapByteSize(size.getWidth(), size.getHeight(), size.getConfig()); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/prefill/PreFillQueue.java ================================================ package com.bumptech.glide.load.engine.prefill; import java.util.ArrayList; import java.util.List; import java.util.Map; final class PreFillQueue { private final Map bitmapsPerType; private final List keyList; private int bitmapsRemaining; private int keyIndex; public PreFillQueue(Map bitmapsPerType) { this.bitmapsPerType = bitmapsPerType; // We don't particularly care about the initial order. keyList = new ArrayList<>(bitmapsPerType.keySet()); for (Integer count : bitmapsPerType.values()) { bitmapsRemaining += count; } } public PreFillType remove() { PreFillType result = keyList.get(keyIndex); Integer countForResult = bitmapsPerType.get(result); if (countForResult == 1) { bitmapsPerType.remove(result); keyList.remove(keyIndex); } else { bitmapsPerType.put(result, countForResult - 1); } bitmapsRemaining--; // Avoid divide by 0. keyIndex = keyList.isEmpty() ? 0 : (keyIndex + 1) % keyList.size(); return result; } public int getSize() { return bitmapsRemaining; } public boolean isEmpty() { return bitmapsRemaining == 0; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/engine/prefill/PreFillType.java ================================================ package com.bumptech.glide.load.engine.prefill; import android.graphics.Bitmap; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Preconditions; /** * A container for a put of options used to pre-fill a {@link * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} with {@link Bitmap Bitmaps} of a single * size and configuration. */ public final class PreFillType { @VisibleForTesting static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.RGB_565; private final int width; private final int height; private final Bitmap.Config config; private final int weight; /** * Constructor for a single type of {@link android.graphics.Bitmap}. * * @param width The width in pixels of the {@link android.graphics.Bitmap Bitmaps} to pre-fill. * @param height The height in pixels of the {@link android.graphics.Bitmap Bitmaps} to pre-fill. * @param config The {@link android.graphics.Bitmap.Config} of the {@link android.graphics.Bitmap * Bitmaps} to pre-fill. * @param weight An integer indicating how to balance pre-filling this size and configuration of * {@link android.graphics.Bitmap} against any other sizes/configurations that may be being * pre-filled. */ PreFillType(int width, int height, Bitmap.Config config, int weight) { this.config = Preconditions.checkNotNull(config, "Config must not be null"); this.width = width; this.height = height; this.weight = weight; } /** Returns the width in pixels of the {@link android.graphics.Bitmap Bitmaps}. */ int getWidth() { return width; } /** Returns the height in pixels of the {@link android.graphics.Bitmap Bitmaps}. */ int getHeight() { return height; } /** * Returns the {@link android.graphics.Bitmap.Config} of the {@link android.graphics.Bitmap * Bitmaps}. */ Bitmap.Config getConfig() { return config; } /** Returns the weight of the {@link android.graphics.Bitmap Bitmaps} of this type. */ int getWeight() { return weight; } @Override public boolean equals(Object o) { if (o instanceof PreFillType) { PreFillType other = (PreFillType) o; return height == other.height && width == other.width && weight == other.weight && config == other.config; } return false; } @Override public int hashCode() { int result = width; result = 31 * result + height; result = 31 * result + config.hashCode(); result = 31 * result + weight; return result; } @Override public String toString() { return "PreFillSize{" + "width=" + width + ", height=" + height + ", config=" + config + ", weight=" + weight + '}'; } /** Builder for {@link PreFillType}. */ public static class Builder { private final int width; private final int height; private Bitmap.Config config; private int weight = 1; /** * Constructor for a builder that uses the given size as the width and height of the Bitmaps to * prefill. * * @param size The width and height in pixels of the Bitmaps to prefill. */ public Builder(int size) { this(size, size); } /** * Constructor for a builder that uses the given dimensions as the dimensions of the Bitmaps to * prefill. * * @param width The width in pixels of the Bitmaps to prefill. * @param height The height in pixels of the Bitmaps to prefill. */ public Builder(int width, int height) { if (width <= 0) { throw new IllegalArgumentException("Width must be > 0"); } if (height <= 0) { throw new IllegalArgumentException("Height must be > 0"); } this.width = width; this.height = height; } /** * Sets the {@link android.graphics.Bitmap.Config} for the Bitmaps to pre-fill. * * @param config The config to use, or null to use Glide's default. * @return This builder. */ public Builder setConfig(@Nullable Bitmap.Config config) { this.config = config; return this; } /** Returns the current {@link android.graphics.Bitmap.Config}. */ Bitmap.Config getConfig() { return config; } /** * Sets the weight to use to balance how many Bitmaps of this type are prefilled relative to the * other requested types. * * @param weight An integer indicating how to balance pre-filling this size and configuration of * {@link android.graphics.Bitmap} against any other sizes/configurations that may be being * pre-filled. * @return This builder. */ public Builder setWeight(int weight) { if (weight <= 0) { throw new IllegalArgumentException("Weight must be > 0"); } this.weight = weight; return this; } /** Returns a new {@link PreFillType}. */ PreFillType build() { return new PreFillType(width, height, config, weight); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/AssetUriLoader.java ================================================ package com.bumptech.glide.load.model; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.net.Uri; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.FileDescriptorAssetPathFetcher; import com.bumptech.glide.load.data.StreamAssetPathFetcher; import com.bumptech.glide.signature.ObjectKey; import java.io.InputStream; /** * Loads a specific data type from an Asset Manager Uri. * * @param The type of data this loader will obtain. */ public class AssetUriLoader implements ModelLoader { private static final String ASSET_PATH_SEGMENT = "android_asset"; private static final String ASSET_PREFIX = ContentResolver.SCHEME_FILE + ":///" + ASSET_PATH_SEGMENT + "/"; private static final int ASSET_PREFIX_LENGTH = ASSET_PREFIX.length(); private final AssetManager assetManager; private final AssetFetcherFactory factory; // Public API. @SuppressWarnings("WeakerAccess") public AssetUriLoader(AssetManager assetManager, AssetFetcherFactory factory) { this.assetManager = assetManager; this.factory = factory; } @Override public LoadData buildLoadData( @NonNull Uri model, int width, int height, @NonNull Options options) { String assetPath = model.toString().substring(ASSET_PREFIX_LENGTH); return new LoadData<>(new ObjectKey(model), factory.buildFetcher(assetManager, assetPath)); } @Override public boolean handles(@NonNull Uri model) { return ContentResolver.SCHEME_FILE.equals(model.getScheme()) && !model.getPathSegments().isEmpty() && ASSET_PATH_SEGMENT.equals(model.getPathSegments().get(0)); } /** * A factory to build a {@link DataFetcher} for a specific asset path. * * @param The type of data that will be obtained by the fetcher. */ public interface AssetFetcherFactory { DataFetcher buildFetcher(AssetManager assetManager, String assetPath); } /** Factory for loading {@link InputStream}s from asset manager Uris. */ public static class StreamFactory implements ModelLoaderFactory, AssetFetcherFactory { private final AssetManager assetManager; public StreamFactory(AssetManager assetManager) { this.assetManager = assetManager; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new AssetUriLoader<>(assetManager, this); } @Override public void teardown() { // Do nothing. } @Override public DataFetcher buildFetcher(AssetManager assetManager, String assetPath) { return new StreamAssetPathFetcher(assetManager, assetPath); } } /** Factory for loading {@link AssetFileDescriptor}s from asset manager Uris. */ public static class FileDescriptorFactory implements ModelLoaderFactory, AssetFetcherFactory { private final AssetManager assetManager; public FileDescriptorFactory(AssetManager assetManager) { this.assetManager = assetManager; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new AssetUriLoader<>(assetManager, this); } @Override public void teardown() { // Do nothing. } @Override public DataFetcher buildFetcher( AssetManager assetManager, String assetPath) { return new FileDescriptorAssetPathFetcher(assetManager, assetPath); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ByteArrayLoader.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.signature.ObjectKey; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.ByteBuffer; /** * A base class to convert byte arrays to input streams so they can be decoded. This class is * abstract because there is no simple/quick way to generate an id from the bytes themselves, so * subclass must include an id. * * @param The type of data that will be loaded from a given byte array. */ public class ByteArrayLoader implements ModelLoader { private final Converter converter; @SuppressWarnings("WeakerAccess") // Public API public ByteArrayLoader(Converter converter) { this.converter = converter; } @Override public LoadData buildLoadData( @NonNull byte[] model, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(model), new Fetcher<>(model, converter)); } @Override public boolean handles(@NonNull byte[] model) { return true; } /** * Converts between a byte array a desired model class. * * @param The type of data to convert to. */ public interface Converter { Data convert(byte[] model); Class getDataClass(); } private static class Fetcher implements DataFetcher { private final byte[] model; private final Converter converter; /** * @param model We really ought to copy the model, but doing so can be hugely expensive and/or * lead to OOMs. In practice it's unlikely that users would pass an array into Glide and * then mutate it. */ @SuppressWarnings("PMD.ArrayIsStoredDirectly") Fetcher(byte[] model, Converter converter) { this.model = model; this.converter = converter; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { Data result = converter.convert(model); callback.onDataReady(result); } @Override public void cleanup() { // Do nothing. } @Override public void cancel() { // Do nothing. } @NonNull @Override public Class getDataClass() { return converter.getDataClass(); } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } /** * Factory for {@link com.bumptech.glide.load.model.ByteArrayLoader} and {@link * java.nio.ByteBuffer}. */ public static class ByteBufferFactory implements ModelLoaderFactory { @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new ByteArrayLoader<>( new Converter() { @Override public ByteBuffer convert(byte[] model) { return ByteBuffer.wrap(model); } @Override public Class getDataClass() { return ByteBuffer.class; } }); } @Override public void teardown() { // Do nothing. } } /** Factory for {@link ByteArrayLoader} and {@link java.io.InputStream}. */ public static class StreamFactory implements ModelLoaderFactory { @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new ByteArrayLoader<>( new Converter() { @Override public InputStream convert(byte[] model) { return new ByteArrayInputStream(model); } @Override public Class getDataClass() { return InputStream.class; } }); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ByteBufferEncoder.java ================================================ package com.bumptech.glide.load.model; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.load.Encoder; import com.bumptech.glide.load.Options; import com.bumptech.glide.util.ByteBufferUtil; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; /** Writes {@link ByteBuffer ByteBuffers} to {@link File Files}. */ public class ByteBufferEncoder implements Encoder { private static final String TAG = "ByteBufferEncoder"; @Override public boolean encode(@NonNull ByteBuffer data, @NonNull File file, @NonNull Options options) { boolean success = false; try { ByteBufferUtil.toFile(data, file); success = true; } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to write data", e); } } return success; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ByteBufferFileLoader.java ================================================ package com.bumptech.glide.load.model; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.util.ByteBufferUtil; import com.bumptech.glide.util.Synthetic; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; /** Loads {@link java.nio.ByteBuffer}s using NIO for {@link java.io.File}. */ public class ByteBufferFileLoader implements ModelLoader { private static final String TAG = "ByteBufferFileLoader"; @Override public LoadData buildLoadData( @NonNull File file, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(file), new ByteBufferFetcher(file)); } @Override public boolean handles(@NonNull File file) { return true; } /** Factory for {@link com.bumptech.glide.load.model.ByteBufferFileLoader}. */ public static class Factory implements ModelLoaderFactory { @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new ByteBufferFileLoader(); } @Override public void teardown() { // Do nothing. } } private static final class ByteBufferFetcher implements DataFetcher { private final File file; @Synthetic @SuppressWarnings("WeakerAccess") ByteBufferFetcher(File file) { this.file = file; } @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { ByteBuffer result; try { result = ByteBufferUtil.fromFile(file); callback.onDataReady(result); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to obtain ByteBuffer for file", e); } callback.onLoadFailed(e); } } @Override public void cleanup() { // Do nothing. } @Override public void cancel() { // Do nothing. } @NonNull @Override public Class getDataClass() { return ByteBuffer.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/DataUrlLoader.java ================================================ package com.bumptech.glide.load.model; import android.util.Base64; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.signature.ObjectKey; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; /** * A simple model loader for loading data from a Data URL String. * *

    Data URIs use the "data" scheme. * *

    See http://www.ietf.org/rfc/rfc2397.txt for a complete description of the 'data' URL scheme. * *

    Briefly, a 'data' URL has the form: * *

    data:[mediatype][;base64],some_data
    * * @param The type of Model that we can retrieve data for, e.g. {@link String}. * @param The type of data that can be opened, e.g. {@link InputStream}. */ public final class DataUrlLoader implements ModelLoader { private static final String DATA_SCHEME_IMAGE = "data:image"; private static final String BASE64_TAG = ";base64"; private final DataDecoder dataDecoder; // Public API. @SuppressWarnings("WeakerAccess") public DataUrlLoader(DataDecoder dataDecoder) { this.dataDecoder = dataDecoder; } @Override public LoadData buildLoadData( @NonNull Model model, int width, int height, @NonNull Options options) { return new LoadData<>( new ObjectKey(model), new DataUriFetcher<>(model.toString(), dataDecoder)); } @Override public boolean handles(@NonNull Model model) { // We expect Model to be a Uri or a String, both of which implement toString() efficiently. We // should reconsider this implementation before adding any new Model types. return model.toString().startsWith(DATA_SCHEME_IMAGE); } /** * Allows decoding a specific type of data from a Data URL String. * * @param The type of data that can be opened. */ public interface DataDecoder { Data decode(String uri) throws IllegalArgumentException; void close(Data data) throws IOException; Class getDataClass(); } private static final class DataUriFetcher implements DataFetcher { private final String dataUri; private final DataDecoder reader; private Data data; DataUriFetcher(String dataUri, DataDecoder reader) { this.dataUri = dataUri; this.reader = reader; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { data = reader.decode(dataUri); callback.onDataReady(data); } catch (IllegalArgumentException e) { callback.onLoadFailed(e); } } @Override public void cleanup() { try { reader.close(data); } catch (IOException e) { // Ignored. } } @Override public void cancel() { // Do nothing. } @NonNull @Override public Class getDataClass() { return reader.getDataClass(); } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } /** * Factory for loading {@link InputStream}s from data uris. * * @param The type of Model we can obtain data for, e.g. String. */ public static final class StreamFactory implements ModelLoaderFactory { private final DataDecoder opener; public StreamFactory() { opener = new DataDecoder() { @Override public InputStream decode(String url) { if (!url.startsWith(DATA_SCHEME_IMAGE)) { throw new IllegalArgumentException("Not a valid image data URL."); } int commaIndex = url.indexOf(','); if (commaIndex == -1) { throw new IllegalArgumentException("Missing comma in data URL."); } String beforeComma = url.substring(0, commaIndex); if (!beforeComma.endsWith(BASE64_TAG)) { throw new IllegalArgumentException("Not a base64 image data URL."); } String afterComma = url.substring(commaIndex + 1); byte[] bytes = Base64.decode(afterComma, Base64.DEFAULT); return new ByteArrayInputStream(bytes); } @Override public void close(InputStream inputStream) throws IOException { inputStream.close(); } @Override public Class getDataClass() { return InputStream.class; } }; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new DataUrlLoader<>(opener); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/DirectResourceLoader.java ================================================ package com.bumptech.glide.load.model; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Build.VERSION_CODES; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.resource.drawable.DrawableDecoderCompat; import com.bumptech.glide.load.resource.drawable.ResourceDrawableDecoder; import com.bumptech.glide.signature.ObjectKey; import java.io.IOException; import java.io.InputStream; /** * Loads themed resource ids using {@link Resources#openRawResource(int)} or {@link * Resources#openRawResourceFd(int)} using the theme from {@link ResourceDrawableDecoder#THEME} when * it's available. * *

    Resource ids from other packages are handled by {@link ResourceLoader} via {@link * ResourceDrawableDecoder} and {@link * com.bumptech.glide.load.resource.bitmap.ResourceBitmapDecoder}. * * @param The type of data this {@code ModelLoader} will produce (e.g. {@link InputStream}, * {@link AssetFileDescriptor} etc). */ public final class DirectResourceLoader implements ModelLoader { private final Context context; private final ResourceOpener resourceOpener; public static ModelLoaderFactory inputStreamFactory(Context context) { return new InputStreamFactory(context); } public static ModelLoaderFactory assetFileDescriptorFactory( Context context) { return new AssetFileDescriptorFactory(context); } public static ModelLoaderFactory drawableFactory(Context context) { return new DrawableFactory(context); } DirectResourceLoader(Context context, ResourceOpener resourceOpener) { this.context = context.getApplicationContext(); this.resourceOpener = resourceOpener; } @Override public LoadData buildLoadData( @NonNull Integer resourceId, int width, int height, @NonNull Options options) { Theme theme = options.get(ResourceDrawableDecoder.THEME); Resources resources = Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && theme != null ? theme.getResources() : context.getResources(); return new LoadData<>( // TODO(judds): We try to apply AndroidResourceSignature for caching in RequestBuilder. // Arguably we should mix in that information here instead. new ObjectKey(resourceId), new ResourceDataFetcher<>(theme, resources, resourceOpener, resourceId)); } @Override public boolean handles(@NonNull Integer integer) { // We could check that this is in fact a resource ID, but doing so isn't free and in practice // it doesn't seem to have been an issue historically. return true; } private interface ResourceOpener { /** * {@code resources} is expected to come from the given {@code theme}, so {@code theme} does not * need to be used if it's not required. */ DataT open(@Nullable Theme theme, Resources resources, int resourceId); void close(DataT data) throws IOException; Class getDataClass(); } private static final class AssetFileDescriptorFactory implements ModelLoaderFactory, ResourceOpener { private final Context context; AssetFileDescriptorFactory(Context context) { this.context = context; } @Override public AssetFileDescriptor open(@Nullable Theme theme, Resources resources, int resourceId) { return resources.openRawResourceFd(resourceId); } @Override public void close(AssetFileDescriptor data) throws IOException { data.close(); } @Override public Class getDataClass() { return AssetFileDescriptor.class; } @NonNull @Override public ModelLoader build( @NonNull MultiModelLoaderFactory multiFactory) { return new DirectResourceLoader<>(context, this); } @Override public void teardown() {} } private static final class InputStreamFactory implements ModelLoaderFactory, ResourceOpener { private final Context context; InputStreamFactory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new DirectResourceLoader<>(context, this); } @Override public InputStream open(@Nullable Theme theme, Resources resources, int resourceId) { return resources.openRawResource(resourceId); } @Override public void close(InputStream data) throws IOException { data.close(); } @Override public Class getDataClass() { return InputStream.class; } @Override public void teardown() {} } /** * Handles vectors, shapes and other resources that cannot be opened with * Resources.openRawResource. Overlaps in functionality with {@link ResourceDrawableDecoder} and * {@link com.bumptech.glide.load.resource.bitmap.ResourceBitmapDecoder} but it's more efficient * for simple resource loads within a single application. */ private static final class DrawableFactory implements ModelLoaderFactory, ResourceOpener { private final Context context; DrawableFactory(Context context) { this.context = context; } @Override public Drawable open(@Nullable Theme theme, Resources resources, int resourceId) { // The Resources already includes the theme provided with the request, so we don't need to // provide the theme separately. return DrawableDecoderCompat.getDrawable(context, resourceId, theme); } @Override public void close(Drawable data) throws IOException {} @Override public Class getDataClass() { return Drawable.class; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new DirectResourceLoader<>(context, this); } @Override public void teardown() {} } private static final class ResourceDataFetcher implements DataFetcher { @Nullable private final Theme theme; private final Resources resources; private final ResourceOpener resourceOpener; private final int resourceId; @Nullable private DataT data; ResourceDataFetcher( @Nullable Theme theme, Resources resources, ResourceOpener resourceOpener, int resourceId) { this.theme = theme; this.resources = resources; this.resourceOpener = resourceOpener; this.resourceId = resourceId; } @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { try { data = resourceOpener.open(theme, resources, resourceId); callback.onDataReady(data); } catch (Resources.NotFoundException e) { callback.onLoadFailed(e); } } @Override public void cleanup() { DataT local = data; if (local != null) { try { resourceOpener.close(local); } catch (IOException e) { // Ignored. } } } @Override public void cancel() {} @NonNull @Override public Class getDataClass() { return resourceOpener.getDataClass(); } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/FileLoader.java ================================================ package com.bumptech.glide.load.model; import android.os.ParcelFileDescriptor; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.signature.ObjectKey; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; /** * A simple model loader for loading data from {@link File}s. * * @param The type of data loaded from the given {@link java.io.File} ({@link * java.io.InputStream} or {@link java.io.FileDescriptor} etc). */ public class FileLoader implements ModelLoader { private static final String TAG = "FileLoader"; private final FileOpener fileOpener; // Public API. @SuppressWarnings("WeakerAccess") public FileLoader(FileOpener fileOpener) { this.fileOpener = fileOpener; } @Override public LoadData buildLoadData( @NonNull File model, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(model), new FileFetcher<>(model, fileOpener)); } @Override public boolean handles(@NonNull File model) { return true; } /** * Allows opening a specific type of data from a {@link java.io.File}. * * @param The type of data that can be opened. */ public interface FileOpener { Data open(File file) throws FileNotFoundException; void close(Data data) throws IOException; Class getDataClass(); } private static final class FileFetcher implements DataFetcher { private final File file; private final FileOpener opener; private Data data; FileFetcher(File file, FileOpener opener) { this.file = file; this.opener = opener; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { data = opener.open(file); callback.onDataReady(data); } catch (FileNotFoundException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to open file", e); } callback.onLoadFailed(e); } } @Override public void cleanup() { if (data != null) { try { opener.close(data); } catch (IOException e) { // Ignored. } } } @Override public void cancel() { // Do nothing. } @NonNull @Override public Class getDataClass() { return opener.getDataClass(); } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } /** * Base factory for loading data from {@link java.io.File files}. * * @param The type of data that will be loaded for a given {@link java.io.File}. */ public static class Factory implements ModelLoaderFactory { private final FileOpener opener; public Factory(FileOpener opener) { this.opener = opener; } @NonNull @Override public final ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new FileLoader<>(opener); } @Override public final void teardown() { // Do nothing. } } /** Factory for loading {@link InputStream}s from {@link File}s. */ public static class StreamFactory extends Factory { public StreamFactory() { super( new FileOpener() { @Override public InputStream open(File file) throws FileNotFoundException { return new FileInputStream(file); } @Override public void close(InputStream inputStream) throws IOException { inputStream.close(); } @Override public Class getDataClass() { return InputStream.class; } }); } } /** Factory for loading {@link ParcelFileDescriptor}s from {@link File}s. */ public static class FileDescriptorFactory extends Factory { public FileDescriptorFactory() { super( new FileOpener() { @Override public ParcelFileDescriptor open(File file) throws FileNotFoundException { return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } @Override public void close(ParcelFileDescriptor parcelFileDescriptor) throws IOException { parcelFileDescriptor.close(); } @Override public Class getDataClass() { return ParcelFileDescriptor.class; } }); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/GlideUrl.java ================================================ package com.bumptech.glide.load.model; import android.net.Uri; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import com.bumptech.glide.util.Preconditions; import java.net.MalformedURLException; import java.net.URL; import java.security.MessageDigest; import java.util.Map; /** * A wrapper for strings representing http/https URLs responsible for ensuring URLs are properly * escaped and avoiding unnecessary URL instantiations for loaders that require only string urls * rather than URL objects. * *

    Users wishing to replace the class for handling URLs must register a factory using GlideUrl. * *

    To obtain a properly escaped URL, call {@link #toURL()}. To obtain a properly escaped string * URL, call {@link #toStringUrl()}. To obtain a less safe, but less expensive to calculate cache * key, call {@link #getCacheKey()}. * *

    This class can also optionally wrap {@link com.bumptech.glide.load.model.Headers} for * convenience. */ public class GlideUrl implements Key { private static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%;$[]"; private final Headers headers; @Nullable private final URL url; @Nullable private final String stringUrl; @Nullable private String safeStringUrl; @Nullable private URL safeUrl; @Nullable private volatile byte[] cacheKeyBytes; private int hashCode; public GlideUrl(URL url) { this(url, Headers.DEFAULT); } public GlideUrl(String url) { this(url, Headers.DEFAULT); } public GlideUrl(URL url, Headers headers) { this.url = Preconditions.checkNotNull(url); stringUrl = null; this.headers = Preconditions.checkNotNull(headers); } public GlideUrl(String url, Headers headers) { this.url = null; this.stringUrl = Preconditions.checkNotEmpty(url); this.headers = Preconditions.checkNotNull(headers); } public URL toURL() throws MalformedURLException { return getSafeUrl(); } // See http://stackoverflow.com/questions/3286067/url-encoding-in-android. Although the answer // using URI would work, using it would require both decoding and encoding each string which is // more complicated, slower and generates more objects than the solution below. See also issue // #133. private URL getSafeUrl() throws MalformedURLException { if (safeUrl == null) { safeUrl = new URL(getSafeStringUrl()); } return safeUrl; } /** * Returns a properly escaped {@link String} url that can be used to make http/https requests. * * @see #toURL() * @see #getCacheKey() */ public String toStringUrl() { return getSafeStringUrl(); } private String getSafeStringUrl() { if (TextUtils.isEmpty(safeStringUrl)) { String unsafeStringUrl = stringUrl; if (TextUtils.isEmpty(unsafeStringUrl)) { unsafeStringUrl = Preconditions.checkNotNull(url).toString(); } safeStringUrl = Uri.encode(unsafeStringUrl, ALLOWED_URI_CHARS); } return safeStringUrl; } /** Returns a non-null {@link Map} containing headers. */ public Map getHeaders() { return headers.getHeaders(); } /** * Returns an inexpensive to calculate {@link String} suitable for use as a disk cache key. * *

    This method does not include headers. * *

    Unlike {@link #toStringUrl()}} and {@link #toURL()}, this method does not escape input. */ // Public API. @SuppressWarnings("WeakerAccess") public String getCacheKey() { return stringUrl != null ? stringUrl : Preconditions.checkNotNull(url).toString(); } @Override public String toString() { return getCacheKey(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(getCacheKeyBytes()); } private byte[] getCacheKeyBytes() { if (cacheKeyBytes == null) { cacheKeyBytes = getCacheKey().getBytes(CHARSET); } return cacheKeyBytes; } @Override public boolean equals(Object o) { if (o instanceof GlideUrl) { GlideUrl other = (GlideUrl) o; return getCacheKey().equals(other.getCacheKey()) && headers.equals(other.headers); } return false; } @Override public int hashCode() { if (hashCode == 0) { hashCode = getCacheKey().hashCode(); hashCode = 31 * hashCode + headers.hashCode(); } return hashCode; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/Headers.java ================================================ package com.bumptech.glide.load.model; import java.util.Collections; import java.util.Map; /** * An interface for a wrapper for a set of headers to be included in a Glide request. * *

    Implementations must implement equals() and hashcode(). */ public interface Headers { /** * An empty Headers object that can be used if users don't want to provide headers. * * @deprecated Use {@link #DEFAULT} instead. */ @Deprecated Headers NONE = new Headers() { @Override public Map getHeaders() { return Collections.emptyMap(); } }; /** * A Headers object containing reasonable defaults that should be used when users don't want to * provide their own headers. */ Headers DEFAULT = new LazyHeaders.Builder().build(); /** Returns a non-null map containing a set of headers to apply to an http request. */ Map getHeaders(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/LazyHeaderFactory.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.Nullable; /** * An interface for lazily creating headers that allows expensive to calculate headers (oauth for * example) to be generated in the background during the first fetch. * *

    Implementations should implement equals() and hashcode() . */ public interface LazyHeaderFactory { /** * Returns an http header, or {@code null} if no header could be built. * *

    Returning {@code null} or an empty String from this method will result in this particular * key/value being excluded from the headers provided in the request. If there are multiple * factories or values for a particular key, any non-null values will still be included for that * key. */ @Nullable String buildHeader(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/LazyHeaders.java ================================================ package com.bumptech.glide.load.model; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A wrapper class for a set of headers to be included in a Glide request, allowing headers to be * constructed lazily. * *

    Ideally headers are constructed once and then re-used for multiple loads, rather then being * constructed individually for each load. * *

    This class is thread safe. */ public final class LazyHeaders implements Headers { private final Map> headers; private volatile Map combinedHeaders; LazyHeaders(Map> headers) { this.headers = Collections.unmodifiableMap(headers); } @Override public Map getHeaders() { if (combinedHeaders == null) { synchronized (this) { if (combinedHeaders == null) { this.combinedHeaders = Collections.unmodifiableMap(generateHeaders()); } } } return combinedHeaders; } private Map generateHeaders() { Map combinedHeaders = new HashMap<>(); for (Map.Entry> entry : headers.entrySet()) { String values = buildHeaderValue(entry.getValue()); if (!TextUtils.isEmpty(values)) { combinedHeaders.put(entry.getKey(), values); } } return combinedHeaders; } @NonNull private String buildHeaderValue(@NonNull List factories) { StringBuilder sb = new StringBuilder(); int size = factories.size(); for (int i = 0; i < size; i++) { LazyHeaderFactory factory = factories.get(i); String header = factory.buildHeader(); if (!TextUtils.isEmpty(header)) { sb.append(header); if (i != factories.size() - 1) { sb.append(','); } } } return sb.toString(); } @Override public String toString() { return "LazyHeaders{" + "headers=" + headers + '}'; } @Override public boolean equals(Object o) { if (o instanceof LazyHeaders) { LazyHeaders other = (LazyHeaders) o; return headers.equals(other.headers); } return false; } @Override public int hashCode() { return headers.hashCode(); } /** * Adds an {@link LazyHeaderFactory} that will be used to construct a value for the given key* * lazily on a background thread. * *

    This class is not thread safe. * *

    This class may include default values for User-Agent and Accept-Encoding headers. These will * be replaced by calls to either {@link #setHeader(String, LazyHeaderFactory)} or {@link * #addHeader(String, String)}, even though {@link #addHeader(String, LazyHeaderFactory)} would * usually append an additional value. */ public static final class Builder { private static final String USER_AGENT_HEADER = "User-Agent"; private static final String DEFAULT_USER_AGENT = getSanitizedUserAgent(); private static final Map> DEFAULT_HEADERS; // Set Accept-Encoding header to do our best to avoid gzip since it's both inefficient for // images and also makes it more difficult for us to detect and prevent partial content // rendering. See #440. static { Map> temp = new HashMap<>(2); if (!TextUtils.isEmpty(DEFAULT_USER_AGENT)) { temp.put( USER_AGENT_HEADER, Collections.singletonList( new StringHeaderFactory(DEFAULT_USER_AGENT))); } DEFAULT_HEADERS = Collections.unmodifiableMap(temp); } private boolean copyOnModify = true; private Map> headers = DEFAULT_HEADERS; private boolean isUserAgentDefault = true; /** * Adds a value for the given header and returns this builder. * *

    Use {@link #addHeader(String, LazyHeaderFactory)} if obtaining the value requires I/O * (i.e. an OAuth token). * * @see #addHeader(String, LazyHeaderFactory) */ public Builder addHeader(@NonNull String key, @NonNull String value) { return addHeader(key, new StringHeaderFactory(value)); } /** * Adds an {@link LazyHeaderFactory} that will be used to construct a value for the given key * lazily on a background thread. * *

    Headers may have multiple values whose order is defined by the order in which this method * is called. * *

    This class does not prevent you from adding the same value to a given key multiple times */ public Builder addHeader(@NonNull String key, @NonNull LazyHeaderFactory factory) { if (isUserAgentDefault && USER_AGENT_HEADER.equalsIgnoreCase(key)) { return setHeader(key, factory); } copyIfNecessary(); getFactories(key).add(factory); return this; } /** * Replaces all existing {@link LazyHeaderFactory LazyHeaderFactorys} for the given key with the * given {@link LazyHeaderFactory}. * *

    If the given value is {@code null}, the header at the given key will be removed. * *

    Use {@link #setHeader(String, LazyHeaderFactory)} if obtaining the value requires I/O * (i.e. an OAuth token). */ @SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) // Public API public Builder setHeader(@NonNull String key, @Nullable String value) { return setHeader(key, value == null ? null : new StringHeaderFactory(value)); } /** * Replaces all existing {@link LazyHeaderFactory LazyHeaderFactorys} for the given key with the * given {@link LazyHeaderFactory}. * *

    If the given value is {@code null}, the header at the given key will be removed. */ public Builder setHeader(@NonNull String key, @Nullable LazyHeaderFactory factory) { copyIfNecessary(); if (factory == null) { headers.remove(key); } else { List factories = getFactories(key); factories.clear(); factories.add(factory); } if (isUserAgentDefault && USER_AGENT_HEADER.equalsIgnoreCase(key)) { isUserAgentDefault = false; } return this; } private List getFactories(String key) { List factories = headers.get(key); if (factories == null) { factories = new ArrayList<>(); headers.put(key, factories); } return factories; } private void copyIfNecessary() { if (copyOnModify) { copyOnModify = false; headers = copyHeaders(); } } /** Returns a new immutable {@link LazyHeaders} object. */ public LazyHeaders build() { copyOnModify = true; return new LazyHeaders(headers); } private Map> copyHeaders() { Map> result = new HashMap<>(headers.size()); for (Map.Entry> entry : headers.entrySet()) { @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") List valueCopy = new ArrayList<>(entry.getValue()); result.put(entry.getKey(), valueCopy); } return result; } /** * Ensures that the default header will pass OkHttp3's checks for header values. * * @see #2331 */ @VisibleForTesting static String getSanitizedUserAgent() { String defaultUserAgent = System.getProperty("http.agent"); if (TextUtils.isEmpty(defaultUserAgent)) { return defaultUserAgent; } int length = defaultUserAgent.length(); StringBuilder sb = new StringBuilder(defaultUserAgent.length()); for (int i = 0; i < length; i++) { char c = defaultUserAgent.charAt(i); if ((c > '\u001f' || c == '\t') && c < '\u007f') { sb.append(c); } else { sb.append('?'); } } return sb.toString(); } } static final class StringHeaderFactory implements LazyHeaderFactory { @NonNull private final String value; StringHeaderFactory(@NonNull String value) { this.value = value; } @Override public String buildHeader() { return value; } @Override public String toString() { return "StringHeaderFactory{" + "value='" + value + '\'' + '}'; } @Override public boolean equals(Object o) { if (o instanceof StringHeaderFactory) { StringHeaderFactory other = (StringHeaderFactory) o; return value.equals(other.value); } return false; } @Override public int hashCode() { return value.hashCode(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/MediaStoreFileLoader.java ================================================ package com.bumptech.glide.load.model; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; import android.text.TextUtils; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import com.bumptech.glide.signature.ObjectKey; import java.io.File; import java.io.FileNotFoundException; /** Loads the file path for {@link MediaStore} owned {@link Uri uris}. */ public final class MediaStoreFileLoader implements ModelLoader { private final Context context; // Public API. @SuppressWarnings("WeakerAccess") public MediaStoreFileLoader(Context context) { this.context = context; } @Override public LoadData buildLoadData( @NonNull Uri uri, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(uri), new FilePathFetcher(context, uri)); } @Override public boolean handles(@NonNull Uri uri) { return MediaStoreUtil.isMediaStoreUri(uri); } private static class FilePathFetcher implements DataFetcher { private static final String[] PROJECTION = new String[] { MediaStore.MediaColumns.DATA, }; private final Context context; private final Uri uri; FilePathFetcher(Context context, Uri uri) { this.context = context; this.uri = uri; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { Cursor cursor = context .getContentResolver() .query( uri, PROJECTION, null /*selection*/, null /*selectionArgs*/, null /*sortOrder*/); String filePath = null; if (cursor != null) { try { if (cursor.moveToFirst()) { filePath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)); } } finally { cursor.close(); } } if (TextUtils.isEmpty(filePath)) { callback.onLoadFailed(new FileNotFoundException("Failed to find file path for: " + uri)); } else { callback.onDataReady(new File(filePath)); } } @Override public void cleanup() { // Do nothing. } @Override public void cancel() { // Do nothing. } @NonNull @Override public Class getDataClass() { return File.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } /** {@link ModelLoaderFactory} for {@link MediaStoreFileLoader}s. */ public static final class Factory implements ModelLoaderFactory { private final Context context; public Factory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new MediaStoreFileLoader(context); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/Model.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.Nullable; /** An optional interface that models can implement to enhance control over Glide behaviors. */ public interface Model { /** * Returns {@code true} if this model produces the same image using the same mechanism (server, * authentication, source etc) as the given model. * *

    Models must also override {@link Object#equals(Object other)} and {@link Object#hashCode()} * to ensure that caching functions correctly. If this object returns {@code true} from this * method for a given Model, it must also be equal to and have the same hash code as the given * model. * *

    However, this model may be equal to and have the same hash code as a given model but still * return {@code false} from this method. This method optionally allows you to differentiate * between Models that load the same image via multiple different means. For example one Model * might load the image from server A and another model might load the same image from server B. * The models must be equal to each other with the same hash code because they load the same * image. However two requests made with the different models are not exactly the same because the * way the image is loaded will differ. */ boolean isEquivalentTo(@Nullable Object other); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ModelCache.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.LruCache; import com.bumptech.glide.util.Util; import java.util.Queue; /** * A simple cache that can be used by {@link ModelLoader} and {@link ModelLoaderFactory} to cache * some data for a given model, width and height. For a loader that takes a model and returns a url, * the cache could be used to safely memoize url creation based on the width and height of the view. * * @param Some Model type that implements {@link #equals} and {@link #hashCode}. * @param Some useful type that may be expensive to create (URL, file path, etc). */ public class ModelCache { private static final int DEFAULT_SIZE = 250; private final LruCache, B> cache; // Public API. @SuppressWarnings("unused") public ModelCache() { this(DEFAULT_SIZE); } public ModelCache(long size) { cache = new LruCache, B>(size) { @Override protected void onItemEvicted(@NonNull ModelKey key, @Nullable B item) { key.release(); } }; } /** * Get a value. * * @param model The model. * @param width The width in pixels of the view the image is being loaded into. * @param height The height in pixels of the view the image is being loaded into. * @return The cached result, or null. */ @Nullable public B get(A model, int width, int height) { ModelKey key = ModelKey.get(model, width, height); B result = cache.get(key); key.release(); return result; } /** * Add a value. * * @param model The model. * @param width The width in pixels of the view the image is being loaded into. * @param height The height in pixels of the view the image is being loaded into. * @param value The value to store. */ public void put(A model, int width, int height, B value) { ModelKey key = ModelKey.get(model, width, height); cache.put(key, value); } /** Removes all entries from the cache. */ public void clear() { cache.clearMemory(); } @VisibleForTesting static final class ModelKey { private static final Queue> KEY_QUEUE = Util.createQueue(0); private int height; private int width; private A model; @SuppressWarnings("unchecked") static ModelKey get(A model, int width, int height) { ModelKey modelKey; synchronized (KEY_QUEUE) { modelKey = (ModelKey) KEY_QUEUE.poll(); } if (modelKey == null) { modelKey = new ModelKey<>(); } modelKey.init(model, width, height); return modelKey; } private ModelKey() {} private void init(A model, int width, int height) { this.model = model; this.width = width; this.height = height; } public void release() { synchronized (KEY_QUEUE) { KEY_QUEUE.offer(this); } } @Override public boolean equals(Object o) { if (o instanceof ModelKey) { @SuppressWarnings("unchecked") ModelKey other = (ModelKey) o; return width == other.width && height == other.height && model.equals(other.model); } return false; } @Override public int hashCode() { int result = height; result = 31 * result + width; result = 31 * result + model.hashCode(); return result; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ModelLoader.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.util.Preconditions; import java.util.Collections; import java.util.List; /** * A factory interface for translating an arbitrarily complex data model into a concrete data type * that can be used by an {@link DataFetcher} to obtain the data for a resource represented by the * model. * *

    This interface has two objectives: 1. To translate a specific model into a data type that can * be decoded into a resource. * *

    2. To allow a model to be combined with the dimensions of the view to fetch a resource of a * specific size. * *

    This not only avoids having to duplicate dimensions in xml and in your code in order to * determine the size of a view on devices with different densities, but also allows you to use * layout weights or otherwise programmatically put the dimensions of the view without forcing you * to fetch a generic resource size. * *

    The smaller the resource you fetch, the less bandwidth and battery life you use, and the lower * your memory footprint per resource. * * @param The type of the model. * @param The type of the data that can be used by a {@link * com.bumptech.glide.load.ResourceDecoder} to decode a resource. */ public interface ModelLoader { /** * Contains a set of {@link com.bumptech.glide.load.Key Keys} identifying the source of the load, * alternate cache keys pointing to equivalent data, and a {@link * com.bumptech.glide.load.data.DataFetcher} that can be used to fetch data not found in cache. * * @param The type of data that well be loaded. */ class LoadData { public final Key sourceKey; public final List alternateKeys; public final DataFetcher fetcher; public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher fetcher) { this(sourceKey, Collections.emptyList(), fetcher); } public LoadData( @NonNull Key sourceKey, @NonNull List alternateKeys, @NonNull DataFetcher fetcher) { this.sourceKey = Preconditions.checkNotNull(sourceKey); this.alternateKeys = Preconditions.checkNotNull(alternateKeys); this.fetcher = Preconditions.checkNotNull(fetcher); } } /** * Returns a {@link com.bumptech.glide.load.model.ModelLoader.LoadData} containing a {@link * com.bumptech.glide.load.data.DataFetcher} required to decode the resource represented by this * model, as well as a set of {@link com.bumptech.glide.load.Key Keys} that identify the data * loaded by the {@link com.bumptech.glide.load.data.DataFetcher} as well as an optional list of * alternate keys from which equivalent data can be loaded. The {@link DataFetcher} will not be * used if the resource is already cached. * *

    Note - If no valid data fetcher can be returned (for example if a model has a null URL), * then it is acceptable to return a null data fetcher from this method. * * @param model The model representing the resource. * @param width The width in pixels of the view or target the resource will be loaded into, or * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate that the * resource should be loaded at its original width. * @param height The height in pixels of the view or target the resource will be loaded into, or * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate that the * resource should be loaded at its original height. */ @Nullable LoadData buildLoadData( @NonNull Model model, int width, int height, @NonNull Options options); /** * Returns true if the given model is a of a recognized type that this loader can probably load. * *

    For example, you may want multiple Uri to InputStream loaders. One might handle media store * Uris, another might handle asset Uris, and a third might handle file Uris etc. * *

    This method is generally expected to do no I/O and complete quickly, so best effort results * are acceptable. {@link ModelLoader ModelLoaders} that return true from this method may return * {@code null} from {@link #buildLoadData(Object, int, int, Options)} */ boolean handles(@NonNull Model model); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ModelLoaderFactory.java ================================================ package com.bumptech.glide.load.model; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; /** * An interface for creating a {@link ModelLoader} for a given model type. * *

    The application {@link android.content.Context} can be passed in to the constructor of the * factory when necessary. It's unsafe to retain {@link android.app.Activity} {@link * android.content.Context}s in factories. The {@link android.content.Context} can be obtained from * {@link com.bumptech.glide.module.LibraryGlideModule#registerComponents(Context, Glide, Registry)} * in most cases. * * @param The type of the model the {@link com.bumptech.glide.load.model.ModelLoader}s built by * this factory can handle * @param The type of data the {@link com.bumptech.glide.load.model.ModelLoader}s built by this * factory can load. */ public interface ModelLoaderFactory { /** * Build a concrete ModelLoader for this model type. * * @param multiFactory A map of classes to factories that can be used to construct additional * {@link ModelLoader}s that this factory's {@link ModelLoader} may depend on * @return A new {@link ModelLoader} */ @NonNull ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory); /** A lifecycle method that will be called when this factory is about to replaced. */ void teardown(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ModelLoaderRegistry.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pools.Pool; import com.bumptech.glide.Registry.NoModelLoaderAvailableException; import com.bumptech.glide.util.Synthetic; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Maintains an ordered put of {@link ModelLoader}s and the model and data types they handle in * order from highest priority to lowest. */ // Hides Model throughout. @SuppressWarnings("TypeParameterHidesVisibleType") public class ModelLoaderRegistry { private final MultiModelLoaderFactory multiModelLoaderFactory; private final ModelLoaderCache cache = new ModelLoaderCache(); public ModelLoaderRegistry(@NonNull Pool> throwableListPool) { this(new MultiModelLoaderFactory(throwableListPool)); } private ModelLoaderRegistry(@NonNull MultiModelLoaderFactory multiModelLoaderFactory) { this.multiModelLoaderFactory = multiModelLoaderFactory; } public synchronized void append( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { multiModelLoaderFactory.append(modelClass, dataClass, factory); cache.clear(); } public synchronized void prepend( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { multiModelLoaderFactory.prepend(modelClass, dataClass, factory); cache.clear(); } public synchronized void remove( @NonNull Class modelClass, @NonNull Class dataClass) { tearDown(multiModelLoaderFactory.remove(modelClass, dataClass)); cache.clear(); } public synchronized void replace( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { tearDown(multiModelLoaderFactory.replace(modelClass, dataClass, factory)); cache.clear(); } private void tearDown( @NonNull List> factories) { for (ModelLoaderFactory factory : factories) { factory.teardown(); } } // We're allocating in a loop to avoid allocating empty lists that will never have anything added // to them. @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @NonNull public List> getModelLoaders(@NonNull A model) { List> modelLoaders = getModelLoadersForClass(getClass(model)); if (modelLoaders.isEmpty()) { throw new NoModelLoaderAvailableException(model); } int size = modelLoaders.size(); boolean isEmpty = true; List> filteredLoaders = Collections.emptyList(); //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0; i < size; i++) { ModelLoader loader = modelLoaders.get(i); if (loader.handles(model)) { if (isEmpty) { filteredLoaders = new ArrayList<>(size - i); isEmpty = false; } filteredLoaders.add(loader); } } if (filteredLoaders.isEmpty()) { throw new NoModelLoaderAvailableException(model, modelLoaders); } return filteredLoaders; } public synchronized ModelLoader build( @NonNull Class modelClass, @NonNull Class dataClass) { return multiModelLoaderFactory.build(modelClass, dataClass); } @NonNull public synchronized List> getDataClasses(@NonNull Class modelClass) { return multiModelLoaderFactory.getDataClasses(modelClass); } @NonNull private synchronized List> getModelLoadersForClass( @NonNull Class modelClass) { List> loaders = cache.get(modelClass); if (loaders == null) { loaders = Collections.unmodifiableList(multiModelLoaderFactory.build(modelClass)); cache.put(modelClass, loaders); } return loaders; } @NonNull @SuppressWarnings("unchecked") private static Class getClass(@NonNull A model) { return (Class) model.getClass(); } private static class ModelLoaderCache { private final Map, Entry> cachedModelLoaders = new HashMap<>(); @Synthetic ModelLoaderCache() {} public void clear() { cachedModelLoaders.clear(); } public void put(Class modelClass, List> loaders) { Entry previous = cachedModelLoaders.put(modelClass, new Entry<>(loaders)); if (previous != null) { throw new IllegalStateException("Already cached loaders for model: " + modelClass); } } @Nullable @SuppressWarnings("unchecked") public List> get(Class modelClass) { Entry entry = (Entry) cachedModelLoaders.get(modelClass); return entry == null ? null : entry.loaders; } private static class Entry { @Synthetic final List> loaders; public Entry(List> loaders) { this.loaders = loaders; } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/MultiModelLoader.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pools.Pool; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.DataFetcher.DataCallback; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.util.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Allows attempting multiple ModelLoaders registered for a given model and data class. * *

    TODO: we should try to find a way to remove this class. It exists to allow individual * ModelLoaders to delegate to multiple ModelLoaders without having to duplicate this logic * everywhere. We have very similar logic in the {@link * com.bumptech.glide.load.engine.DataFetcherGenerator} implementations and should try to avoid this * duplication. */ class MultiModelLoader implements ModelLoader { private final List> modelLoaders; private final Pool> exceptionListPool; MultiModelLoader( @NonNull List> modelLoaders, @NonNull Pool> exceptionListPool) { this.modelLoaders = modelLoaders; this.exceptionListPool = exceptionListPool; } @Override public LoadData buildLoadData( @NonNull Model model, int width, int height, @NonNull Options options) { Key sourceKey = null; int size = modelLoaders.size(); List> fetchers = new ArrayList<>(size); //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0; i < size; i++) { ModelLoader modelLoader = modelLoaders.get(i); if (modelLoader.handles(model)) { LoadData loadData = modelLoader.buildLoadData(model, width, height, options); if (loadData != null) { sourceKey = loadData.sourceKey; fetchers.add(loadData.fetcher); } } } return !fetchers.isEmpty() && sourceKey != null ? new LoadData<>(sourceKey, new MultiFetcher<>(fetchers, exceptionListPool)) : null; } @Override public boolean handles(@NonNull Model model) { for (ModelLoader modelLoader : modelLoaders) { if (modelLoader.handles(model)) { return true; } } return false; } @Override public String toString() { return "MultiModelLoader{" + "modelLoaders=" + Arrays.toString(modelLoaders.toArray()) + '}'; } static class MultiFetcher implements DataFetcher, DataCallback { private final List> fetchers; private final Pool> throwableListPool; private int currentIndex; private Priority priority; private DataCallback callback; @Nullable private List exceptions; private boolean isCancelled; MultiFetcher( @NonNull List> fetchers, @NonNull Pool> throwableListPool) { this.throwableListPool = throwableListPool; Preconditions.checkNotEmpty(fetchers); this.fetchers = fetchers; currentIndex = 0; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { this.priority = priority; this.callback = callback; exceptions = throwableListPool.acquire(); fetchers.get(currentIndex).loadData(priority, this); // If a race occurred where we cancelled the fetcher in cancel() and then called loadData here // immediately after, make sure that we cancel the newly started fetcher. We don't bother // checking cancelled before loadData because it's not required for correctness and would // require an unlikely race to be useful. if (isCancelled) { cancel(); } } @Override public void cleanup() { if (exceptions != null) { throwableListPool.release(exceptions); } exceptions = null; for (DataFetcher fetcher : fetchers) { fetcher.cleanup(); } } @Override public void cancel() { isCancelled = true; for (DataFetcher fetcher : fetchers) { fetcher.cancel(); } } @NonNull @Override public Class getDataClass() { return fetchers.get(0).getDataClass(); } @NonNull @Override public DataSource getDataSource() { return fetchers.get(0).getDataSource(); } @Override public void onDataReady(@Nullable Data data) { if (data != null) { callback.onDataReady(data); } else { startNextOrFail(); } } @Override public void onLoadFailed(@NonNull Exception e) { Preconditions.checkNotNull(exceptions).add(e); startNextOrFail(); } private void startNextOrFail() { if (isCancelled) { return; } if (currentIndex < fetchers.size() - 1) { currentIndex++; loadData(priority, callback); } else { Preconditions.checkNotNull(exceptions); callback.onLoadFailed(new GlideException("Fetch failed", new ArrayList<>(exceptions))); } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/MultiModelLoaderFactory.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.util.Pools.Pool; import com.bumptech.glide.Registry.NoModelLoaderAvailableException; import com.bumptech.glide.load.Options; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Capable of building an {@link ModelLoader} that wraps one or more other {@link ModelLoader}s for * a given model and data class. */ // Hides Model throughout. @SuppressWarnings("TypeParameterHidesVisibleType") public class MultiModelLoaderFactory { private static final Factory DEFAULT_FACTORY = new Factory(); private static final ModelLoader EMPTY_MODEL_LOADER = new EmptyModelLoader(); private final List> entries = new ArrayList<>(); private final Factory factory; private final Set> alreadyUsedEntries = new HashSet<>(); private final Pool> throwableListPool; public MultiModelLoaderFactory(@NonNull Pool> throwableListPool) { this(throwableListPool, DEFAULT_FACTORY); } @VisibleForTesting MultiModelLoaderFactory( @NonNull Pool> throwableListPool, @NonNull Factory factory) { this.throwableListPool = throwableListPool; this.factory = factory; } synchronized void append( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { add(modelClass, dataClass, factory, /* append= */ true); } synchronized void prepend( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { add(modelClass, dataClass, factory, /* append= */ false); } private void add( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory, boolean append) { Entry entry = new Entry<>(modelClass, dataClass, factory); entries.add(append ? entries.size() : 0, entry); } @NonNull synchronized List> replace( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { List> removed = remove(modelClass, dataClass); append(modelClass, dataClass, factory); return removed; } @NonNull synchronized List> remove( @NonNull Class modelClass, @NonNull Class dataClass) { List> factories = new ArrayList<>(); for (Iterator> iterator = entries.iterator(); iterator.hasNext(); ) { Entry entry = iterator.next(); if (entry.handles(modelClass, dataClass)) { iterator.remove(); factories.add(this.getFactory(entry)); } } return factories; } @NonNull synchronized List> build(@NonNull Class modelClass) { try { List> loaders = new ArrayList<>(); for (Entry entry : entries) { // Avoid stack overflow recursively creating model loaders by only creating loaders in // recursive requests if they haven't been created earlier in the chain. For example: // A Uri loader may translate to another model, which in turn may translate back to a Uri. // The original Uri loader won't be provided to the intermediate model loader, although // other Uri loaders will be. if (alreadyUsedEntries.contains(entry)) { continue; } if (entry.handles(modelClass)) { alreadyUsedEntries.add(entry); loaders.add(this.build(entry)); alreadyUsedEntries.remove(entry); } } return loaders; } catch (Throwable t) { alreadyUsedEntries.clear(); throw t; } } @NonNull synchronized List> getDataClasses(@NonNull Class modelClass) { List> result = new ArrayList<>(); for (Entry entry : entries) { if (!result.contains(entry.dataClass) && entry.handles(modelClass)) { result.add(entry.dataClass); } } return result; } @NonNull public synchronized ModelLoader build( @NonNull Class modelClass, @NonNull Class dataClass) { try { List> loaders = new ArrayList<>(); boolean ignoredAnyEntries = false; for (Entry entry : entries) { // Avoid stack overflow recursively creating model loaders by only creating loaders in // recursive requests if they haven't been created earlier in the chain. For example: // A Uri loader may translate to another model, which in turn may translate back to a Uri. // The original Uri loader won't be provided to the intermediate model loader, although // other Uri loaders will be. if (alreadyUsedEntries.contains(entry)) { ignoredAnyEntries = true; continue; } if (entry.handles(modelClass, dataClass)) { alreadyUsedEntries.add(entry); loaders.add(this.build(entry)); alreadyUsedEntries.remove(entry); } } if (loaders.size() > 1) { return factory.build(loaders, throwableListPool); } else if (loaders.size() == 1) { return loaders.get(0); } else { // Avoid crashing if recursion results in no loaders available. The assertion is supposed to // catch completely unhandled types, recursion may mean a subtype isn't handled somewhere // down the stack, which is often ok. if (ignoredAnyEntries) { return emptyModelLoader(); } else { throw new NoModelLoaderAvailableException(modelClass, dataClass); } } } catch (Throwable t) { alreadyUsedEntries.clear(); throw t; } } @NonNull @SuppressWarnings("unchecked") private ModelLoaderFactory getFactory(@NonNull Entry entry) { return (ModelLoaderFactory) entry.factory; } @NonNull @SuppressWarnings("unchecked") private ModelLoader build(@NonNull Entry entry) { return (ModelLoader) Preconditions.checkNotNull(entry.factory.build(this)); } @NonNull @SuppressWarnings("unchecked") private static ModelLoader emptyModelLoader() { return (ModelLoader) EMPTY_MODEL_LOADER; } private static class Entry { private final Class modelClass; @Synthetic final Class dataClass; @Synthetic final ModelLoaderFactory factory; public Entry( @NonNull Class modelClass, @NonNull Class dataClass, @NonNull ModelLoaderFactory factory) { this.modelClass = modelClass; this.dataClass = dataClass; this.factory = factory; } public boolean handles(@NonNull Class modelClass, @NonNull Class dataClass) { return handles(modelClass) && this.dataClass.isAssignableFrom(dataClass); } public boolean handles(@NonNull Class modelClass) { return this.modelClass.isAssignableFrom(modelClass); } } static class Factory { @NonNull public MultiModelLoader build( @NonNull List> modelLoaders, @NonNull Pool> throwableListPool) { return new MultiModelLoader<>(modelLoaders, throwableListPool); } } private static class EmptyModelLoader implements ModelLoader { @Synthetic EmptyModelLoader() {} @Nullable @Override public LoadData buildLoadData( @NonNull Object o, int width, int height, @NonNull Options options) { return null; } @Override public boolean handles(@NonNull Object o) { return false; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ResourceLoader.java ================================================ package com.bumptech.glide.load.model; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import java.io.InputStream; /** * A model loader for handling Android resource files. Model must be an Android resource id in the * package of the given context. * *

    This class should always be less preferred than {@link DirectResourceLoader} because {@link * DirectResourceLoader} is more efficient for {@code Drawables} owned by this package. This class * only handles passing through {@link Uri}s to {@link * com.bumptech.glide.load.resource.drawable.ResourceDrawableDecoder} and {@link * com.bumptech.glide.load.resource.bitmap.ResourceBitmapDecoder}. Those classes can handle assets * from other applications, but are not as efficient as {@link DirectResourceLoader} for assets * owned by this package. * * @param The type of data that will be loaded for the given android resource. */ public class ResourceLoader implements ModelLoader { private static final String TAG = "ResourceLoader"; private final ModelLoader uriLoader; private final Resources resources; // Public API. @SuppressWarnings("WeakerAccess") public ResourceLoader(Resources resources, ModelLoader uriLoader) { this.resources = resources; this.uriLoader = uriLoader; } @Override public LoadData buildLoadData( @NonNull Integer model, int width, int height, @NonNull Options options) { Uri uri = getResourceUri(model); return uri == null ? null : uriLoader.buildLoadData(uri, width, height, options); } @Nullable private Uri getResourceUri(Integer model) { try { return Uri.parse( ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + resources.getResourcePackageName(model) + '/' + model); } catch (Resources.NotFoundException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Received invalid resource id: " + model, e); } return null; } } @Override public boolean handles(@NonNull Integer model) { // TODO: check that this is in fact a resource id. return true; } /** Factory for loading {@link InputStream}s from Android resource ids. */ public static class StreamFactory implements ModelLoaderFactory { private final Resources resources; public StreamFactory(Resources resources) { this.resources = resources; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ResourceLoader<>(resources, multiFactory.build(Uri.class, InputStream.class)); } @Override public void teardown() { // Do nothing. } } /** * Factory for loading {@link ParcelFileDescriptor}s from Android resource ids. * * @deprecated This class is unused by Glide. {@link AssetFileDescriptorFactory} should be * preferred because it's not possible to reliably load a simple {@link * java.io.FileDescriptor} for resources. */ @Deprecated public static class FileDescriptorFactory implements ModelLoaderFactory { private final Resources resources; public FileDescriptorFactory(Resources resources) { this.resources = resources; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ResourceLoader<>( resources, multiFactory.build(Uri.class, ParcelFileDescriptor.class)); } @Override public void teardown() { // Do nothing. } } /** Loads {@link AssetFileDescriptor}s from resource ids. */ public static final class AssetFileDescriptorFactory implements ModelLoaderFactory { private final Resources resources; public AssetFileDescriptorFactory(Resources resources) { this.resources = resources; } @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ResourceLoader<>( resources, multiFactory.build(Uri.class, AssetFileDescriptor.class)); } @Override public void teardown() { // Do nothing. } } /** Factory for loading resource {@link Uri}s from Android resource ids. */ public static class UriFactory implements ModelLoaderFactory { private final Resources resources; public UriFactory(Resources resources) { this.resources = resources; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ResourceLoader<>(resources, UnitModelLoader.getInstance()); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/ResourceUriLoader.java ================================================ package com.bumptech.glide.load.model; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import java.io.InputStream; import java.util.List; /** * Converts Resource Uris to resource ids if the resource Uri points to a resource in this package. * *

    This class works by parsing Uris into resource ids, then delegating the resource ID load to * other {@link ModelLoader}s, typically {@link DirectResourceLoader}. * *

    This class really shouldn't need to exist. If you need to load resources, just pass in the * integer resource id directly using {@link com.bumptech.glide.RequestManager#load(Integer)} * instead. It'll be more correct in terms of caching and more efficient to load. The only reason * we're supporting this case is for backwards compatibility. * *

    Because this class explicitly only handles resource Uris that are from the application's * package, resource uris from other packages are handled by {@link UriLoader}. {@link UriLoader} is * even less preferred because it can only handle certain resources from raw resources and it will * not apply appropriate theming, RTL or night mode attributes. * * @param The type of data produced, e.g. {@link InputStream} or {@link * AssetFileDescriptor}. */ public final class ResourceUriLoader implements ModelLoader { /** * See the javadoc on {@link android.content.res.Resources#getIdentifier(java.lang.String, * java.lang.String, java.lang.String)}. */ private static final int INVALID_RESOURCE_ID = 0; private static final String TAG = "ResourceUriLoader"; private final Context context; private final ModelLoader delegate; public static ModelLoaderFactory newStreamFactory(Context context) { return new InputStreamFactory(context); } public static ModelLoaderFactory newAssetFileDescriptorFactory( Context context) { return new AssetFileDescriptorFactory(context); } ResourceUriLoader(Context context, ModelLoader delegate) { this.context = context.getApplicationContext(); this.delegate = delegate; } @Nullable @Override public LoadData buildLoadData( @NonNull Uri uri, int width, int height, @NonNull Options options) { List pathSegments = uri.getPathSegments(); // android.resource/// if (pathSegments.size() == 1) { return parseResourceIdUri(uri, width, height, options); } // android.resource//// if (pathSegments.size() == 2) { return parseResourceNameUri(uri, width, height, options); } if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to parse resource uri: " + uri); } return null; } @Nullable private LoadData parseResourceNameUri( @NonNull Uri uri, int width, int height, @NonNull Options options) { List pathSegments = uri.getPathSegments(); String resourceType = pathSegments.get(0); String resourceName = pathSegments.get(1); // Yes it's bad, but the caller has chosen to give us a resource uri... @SuppressLint("DiscouragedApi") int identifier = context.getResources().getIdentifier(resourceName, resourceType, context.getPackageName()); if (identifier == INVALID_RESOURCE_ID) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to find resource id for: " + uri); } return null; } return delegate.buildLoadData(identifier, width, height, options); } @Nullable private LoadData parseResourceIdUri( @NonNull Uri uri, int width, int height, @NonNull Options options) { try { int resourceId = Integer.parseInt(uri.getPathSegments().get(0)); if (resourceId == INVALID_RESOURCE_ID) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to parse a valid non-0 resource id from: " + uri); } return null; } return delegate.buildLoadData(resourceId, width, height, options); } catch (NumberFormatException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to parse resource id from: " + uri, e); } } return null; } @Override public boolean handles(@NonNull Uri uri) { return ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme()) && context.getPackageName().equals(uri.getAuthority()); } private static final class InputStreamFactory implements ModelLoaderFactory { private final Context context; InputStreamFactory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new ResourceUriLoader<>(context, multiFactory.build(Integer.class, InputStream.class)); } @Override public void teardown() {} } private static final class AssetFileDescriptorFactory implements ModelLoaderFactory { private final Context context; AssetFileDescriptorFactory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build( @NonNull MultiModelLoaderFactory multiFactory) { return new ResourceUriLoader<>( context, multiFactory.build(Integer.class, AssetFileDescriptor.class)); } @Override public void teardown() {} } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/StreamEncoder.java ================================================ package com.bumptech.glide.load.model; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.load.Encoder; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * An {@link com.bumptech.glide.load.Encoder} that can write an {@link java.io.InputStream} to disk. */ public class StreamEncoder implements Encoder { private static final String TAG = "StreamEncoder"; private final ArrayPool byteArrayPool; public StreamEncoder(ArrayPool byteArrayPool) { this.byteArrayPool = byteArrayPool; } @Override public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) { byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class); boolean success = false; OutputStream os = null; try { os = new FileOutputStream(file); int read; while ((read = data.read(buffer)) != -1) { os.write(buffer, 0, read); } os.close(); success = true; } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to encode data onto the OutputStream", e); } } finally { if (os != null) { try { os.close(); } catch (IOException e) { // Do nothing. } } byteArrayPool.put(buffer); } return success; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/StringLoader.java ================================================ package com.bumptech.glide.load.model; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import java.io.File; import java.io.InputStream; /** * A model loader for handling certain string models. Handles paths, urls, and any uri string with a * scheme handled by {@link android.content.ContentResolver#openInputStream(Uri)}. * * @param The type of data that will be loaded from the given {@link java.lang.String}. */ public class StringLoader implements ModelLoader { private final ModelLoader uriLoader; // Public API. @SuppressWarnings("WeakerAccess") public StringLoader(ModelLoader uriLoader) { this.uriLoader = uriLoader; } @Override public LoadData buildLoadData( @NonNull String model, int width, int height, @NonNull Options options) { Uri uri = parseUri(model); if (uri == null || !uriLoader.handles(uri)) { return null; } return uriLoader.buildLoadData(uri, width, height, options); } @Override public boolean handles(@NonNull String model) { // Avoid parsing the Uri twice and simply return null from buildLoadData if we don't handle this // particular Uri type. return true; } @Nullable private static Uri parseUri(String model) { Uri uri; if (TextUtils.isEmpty(model)) { return null; // See https://pmd.github.io/pmd-6.0.0/pmd_rules_java_performance.html#simplifystartswith } else if (model.charAt(0) == '/') { uri = toFileUri(model); } else { uri = Uri.parse(model); String scheme = uri.getScheme(); if (scheme == null) { uri = toFileUri(model); } } return uri; } private static Uri toFileUri(String path) { return Uri.fromFile(new File(path)); } /** Factory for loading {@link InputStream}s from Strings. */ public static class StreamFactory implements ModelLoaderFactory { @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new StringLoader<>(multiFactory.build(Uri.class, InputStream.class)); } @Override public void teardown() { // Do nothing. } } /** Factory for loading {@link ParcelFileDescriptor}s from Strings. */ public static class FileDescriptorFactory implements ModelLoaderFactory { @NonNull @Override public ModelLoader build( @NonNull MultiModelLoaderFactory multiFactory) { return new StringLoader<>(multiFactory.build(Uri.class, ParcelFileDescriptor.class)); } @Override public void teardown() { // Do nothing. } } /** Loads {@link AssetFileDescriptor}s from Strings. */ public static final class AssetFileDescriptorFactory implements ModelLoaderFactory { @Override public ModelLoader build( @NonNull MultiModelLoaderFactory multiFactory) { return new StringLoader<>(multiFactory.build(Uri.class, AssetFileDescriptor.class)); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/UnitModelLoader.java ================================================ package com.bumptech.glide.load.model; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.signature.ObjectKey; /** * A put of helper classes that performs no loading and instead always returns the given model as * the data to decode. * * @param The type of model that will also be returned as decodable data. */ public class UnitModelLoader implements ModelLoader { @SuppressWarnings("deprecation") private static final UnitModelLoader INSTANCE = new UnitModelLoader<>(); @SuppressWarnings("unchecked") public static UnitModelLoader getInstance() { return (UnitModelLoader) INSTANCE; } /** * @deprecated Use {@link #getInstance()} instead. */ // Need constructor to document deprecation, will be removed, when constructor is privatized. @SuppressWarnings({"PMD.UnnecessaryConstructor", "DeprecatedIsStillUsed"}) @Deprecated public UnitModelLoader() { // Intentionally empty. } @Override public LoadData buildLoadData( @NonNull Model model, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(model), new UnitFetcher<>(model)); } @Override public boolean handles(@NonNull Model model) { return true; } private static class UnitFetcher implements DataFetcher { private final Model resource; UnitFetcher(Model resource) { this.resource = resource; } @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { callback.onDataReady(resource); } @Override public void cleanup() { // Do nothing. } @Override public void cancel() { // Do nothing. } @NonNull @SuppressWarnings("unchecked") @Override public Class getDataClass() { return (Class) resource.getClass(); } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } /** * Factory for producing {@link com.bumptech.glide.load.model.UnitModelLoader}s. * * @param The type of model that will also be returned as decodable data. */ // PMD.SingleMethodSingleton false positive: https://github.com/pmd/pmd/issues/816 @SuppressWarnings("PMD.SingleMethodSingleton") public static class Factory implements ModelLoaderFactory { @SuppressWarnings("deprecation") private static final Factory FACTORY = new Factory<>(); @SuppressWarnings("unchecked") public static Factory getInstance() { return (Factory) FACTORY; } /** * @deprecated Use {@link #getInstance()} instead. */ // Need constructor to document deprecation, will be removed, when constructor is privatized. @SuppressWarnings("PMD.UnnecessaryConstructor") @Deprecated public Factory() { // Intentionally empty. } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return UnitModelLoader.getInstance(); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/UriLoader.java ================================================ package com.bumptech.glide.load.model; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.ParcelFileDescriptor; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.AssetFileDescriptorLocalUriFetcher; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.FileDescriptorLocalUriFetcher; import com.bumptech.glide.load.data.StreamLocalUriFetcher; import com.bumptech.glide.signature.ObjectKey; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * A ModelLoader for {@link android.net.Uri}s that handles local {@link android.net.Uri}s directly * and routes remote {@link android.net.Uri}s to a wrapped {@link * com.bumptech.glide.load.model.ModelLoader} that handles {@link * com.bumptech.glide.load.model.GlideUrl}s. * * @param The type of data that will be retrieved for {@link android.net.Uri}s. */ public class UriLoader implements ModelLoader { private static final Set SCHEMES = Collections.unmodifiableSet( new HashSet<>( Arrays.asList( ContentResolver.SCHEME_FILE, ContentResolver.SCHEME_CONTENT, ContentResolver.SCHEME_ANDROID_RESOURCE))); private final LocalUriFetcherFactory factory; // Public API. @SuppressWarnings("WeakerAccess") public UriLoader(LocalUriFetcherFactory factory) { this.factory = factory; } @Override public LoadData buildLoadData( @NonNull Uri model, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(model), factory.build(model)); } @Override public boolean handles(@NonNull Uri model) { return SCHEMES.contains(model.getScheme()); } /** * Factory for obtaining a {@link DataFetcher} for a data type for a particular {@link Uri}. * * @param The type of data the returned {@link DataFetcher} will obtain. */ public interface LocalUriFetcherFactory { DataFetcher build(Uri uri); } /** Loads {@link InputStream}s from {@link Uri}s. */ public static class StreamFactory implements ModelLoaderFactory, LocalUriFetcherFactory { private final ContentResolver contentResolver; private final boolean useMediaStoreApisIfAvailable; public StreamFactory(ContentResolver contentResolver) { this(contentResolver, /* useMediaStoreApisIfAvailable */ false); } /** * useMediaStoreApisIfAvailable is part of an experiment and the constructor can be removed in a * future version. */ public StreamFactory(ContentResolver contentResolver, boolean useMediaStoreApisIfAvailable) { this.contentResolver = contentResolver; this.useMediaStoreApisIfAvailable = useMediaStoreApisIfAvailable; } @Override public DataFetcher build(Uri uri) { return new StreamLocalUriFetcher(contentResolver, uri, useMediaStoreApisIfAvailable); } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new UriLoader<>(this); } @Override public void teardown() { // Do nothing. } } /** Loads {@link ParcelFileDescriptor}s from {@link Uri}s. */ public static class FileDescriptorFactory implements ModelLoaderFactory, LocalUriFetcherFactory { private final ContentResolver contentResolver; private final boolean useMediaStoreApisIfAvailable; public FileDescriptorFactory(ContentResolver contentResolver) { this(contentResolver, /* useMediaStoreApisIfAvailable */ false); } /** * useMediaStoreApisIfAvailable is part of an experiment and the constructor can be removed in a * future version. */ public FileDescriptorFactory( ContentResolver contentResolver, boolean useMediaStoreApisIfAvailable) { this.contentResolver = contentResolver; this.useMediaStoreApisIfAvailable = useMediaStoreApisIfAvailable; } @Override public DataFetcher build(Uri uri) { return new FileDescriptorLocalUriFetcher(contentResolver, uri, useMediaStoreApisIfAvailable); } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new UriLoader<>(this); } @Override public void teardown() { // Do nothing. } } /** Loads {@link AssetFileDescriptor}s from {@link Uri}s. */ public static final class AssetFileDescriptorFactory implements ModelLoaderFactory, LocalUriFetcherFactory { private final ContentResolver contentResolver; private final boolean useMediaStoreApisIfAvailable; public AssetFileDescriptorFactory(ContentResolver contentResolver) { this(contentResolver, /* useMediaStoreApisIfAvailable */ false); } /** * useMediaStoreApisIfAvailable is part of an experiment and the constructor can be removed in a * future version. */ public AssetFileDescriptorFactory( ContentResolver contentResolver, boolean useMediaStoreApisIfAvailable) { this.contentResolver = contentResolver; this.useMediaStoreApisIfAvailable = useMediaStoreApisIfAvailable; } @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new UriLoader<>(this); } @Override public void teardown() { // Do nothing. } @Override public DataFetcher build(Uri uri) { return new AssetFileDescriptorLocalUriFetcher( contentResolver, uri, useMediaStoreApisIfAvailable); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/UrlUriLoader.java ================================================ package com.bumptech.glide.load.model; import android.net.Uri; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * Handles http/https Uris by delegating to the {@link ModelLoader} for {@link * com.bumptech.glide.load.model.GlideUrl GlideUrls}. * * @param The type of data this Loader will obtain for a {@link Uri}. */ public class UrlUriLoader implements ModelLoader { private static final Set SCHEMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("http", "https"))); private final ModelLoader urlLoader; // Public API. @SuppressWarnings("WeakerAccess") public UrlUriLoader(ModelLoader urlLoader) { this.urlLoader = urlLoader; } @Override public LoadData buildLoadData( @NonNull Uri uri, int width, int height, @NonNull Options options) { GlideUrl glideUrl = new GlideUrl(uri.toString()); return urlLoader.buildLoadData(glideUrl, width, height, options); } @Override public boolean handles(@NonNull Uri uri) { return SCHEMES.contains(uri.getScheme()); } /** * Loads {@link java.io.InputStream InputStreams} from {@link android.net.Uri Uris} with http or * https schemes. */ public static class StreamFactory implements ModelLoaderFactory { @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new UrlUriLoader<>(multiFactory.build(GlideUrl.class, InputStream.class)); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/stream/BaseGlideUrlLoader.java ================================================ package com.bumptech.glide.load.model.stream; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.Headers; import com.bumptech.glide.load.model.ModelCache; import com.bumptech.glide.load.model.ModelLoader; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; /** * A base class for loading data over http/https. Can be subclassed for use with any model that can * be translated in to {@link java.io.InputStream} data. * * @param The type of the model. */ public abstract class BaseGlideUrlLoader implements ModelLoader { private final ModelLoader concreteLoader; @Nullable private final ModelCache modelCache; protected BaseGlideUrlLoader(ModelLoader concreteLoader) { this(concreteLoader, null); } protected BaseGlideUrlLoader( ModelLoader concreteLoader, @Nullable ModelCache modelCache) { this.concreteLoader = concreteLoader; this.modelCache = modelCache; } @Override @Nullable public LoadData buildLoadData( @NonNull Model model, int width, int height, @NonNull Options options) { GlideUrl result = null; if (modelCache != null) { result = modelCache.get(model, width, height); } if (result == null) { String stringURL = getUrl(model, width, height, options); if (TextUtils.isEmpty(stringURL)) { return null; } result = new GlideUrl(stringURL, getHeaders(model, width, height, options)); if (modelCache != null) { modelCache.put(model, width, height, result); } } // TODO: this is expensive and slow to calculate every time, we should either cache these, or // try to come up with a way to avoid finding them when not necessary. List alternateUrls = getAlternateUrls(model, width, height, options); LoadData concreteLoaderData = concreteLoader.buildLoadData(result, width, height, options); if (concreteLoaderData == null || alternateUrls.isEmpty()) { return concreteLoaderData; } else { return new LoadData<>( concreteLoaderData.sourceKey, getAlternateKeys(alternateUrls), concreteLoaderData.fetcher); } } // Creating a limited number of objects as the sole purpose of the loop. @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") private static List getAlternateKeys(Collection alternateUrls) { List result = new ArrayList<>(alternateUrls.size()); for (String alternate : alternateUrls) { result.add(new GlideUrl(alternate)); } return result; } /** * Returns a valid url http:// or https:// for the given model and dimensions as a string. * * @param model The model. * @param width The width in pixels of the view/target the image will be loaded into. * @param height The height in pixels of the view/target the image will be loaded into. */ protected abstract String getUrl(Model model, int width, int height, Options options); /** * Returns a list of alternate urls for the given model, width, and height from which equivalent * data can be obtained (usually the same image with the same aspect ratio, but in a larger size) * as the primary url. * *

    Implementing this method allows Glide to fulfill requests for bucketed images in smaller * bucket sizes using already cached data for larger bucket sizes. * * @param width The width in pixels of the view/target the image will be loaded into. * @param height The height in pixels of the view/target the image will be loaded into. */ protected List getAlternateUrls(Model model, int width, int height, Options options) { return Collections.emptyList(); } /** * Returns the headers for the given model and dimensions as a map of strings to sets of strings, * or null if no headers should be added. * * @param model The model. * @param width The width in pixels of the view/target the image will be loaded into. * @param height The height in pixels of the view/target the image will be loaded into. */ // Public API. @SuppressWarnings({"unused", "WeakerAccess"}) @Nullable protected Headers getHeaders(Model model, int width, int height, Options options) { return Headers.DEFAULT; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/stream/HttpGlideUrlLoader.java ================================================ package com.bumptech.glide.load.model.stream; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.HttpUrlFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelCache; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import java.io.InputStream; /** * An {@link com.bumptech.glide.load.model.ModelLoader} for translating {@link * com.bumptech.glide.load.model.GlideUrl} (http/https URLS) into {@link java.io.InputStream} data. */ // Public API. @SuppressWarnings("WeakerAccess") public class HttpGlideUrlLoader implements ModelLoader { /** * An integer option that is used to determine the maximum connect and read timeout durations (in * milliseconds) for network connections. * *

    Defaults to 2500ms. */ public static final Option TIMEOUT = Option.memory("com.bumptech.glide.load.model.stream.HttpGlideUrlLoader.Timeout", 2500); @Nullable private final ModelCache modelCache; public HttpGlideUrlLoader() { this(null); } public HttpGlideUrlLoader(@Nullable ModelCache modelCache) { this.modelCache = modelCache; } @Override public LoadData buildLoadData( @NonNull GlideUrl model, int width, int height, @NonNull Options options) { // GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time // spent parsing urls. GlideUrl url = model; if (modelCache != null) { url = modelCache.get(model, 0, 0); if (url == null) { modelCache.put(model, 0, 0, model); url = model; } } int timeout = options.get(TIMEOUT); return new LoadData<>(url, new HttpUrlFetcher(url, timeout)); } @Override public boolean handles(@NonNull GlideUrl model) { return true; } /** The default factory for {@link HttpGlideUrlLoader}s. */ public static class Factory implements ModelLoaderFactory { private final ModelCache modelCache = new ModelCache<>(500); @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new HttpGlideUrlLoader(modelCache); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/stream/HttpUriLoader.java ================================================ package com.bumptech.glide.load.model.stream; import android.net.Uri; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.UrlUriLoader; import java.io.InputStream; /** * Loads {@link InputStream}s from http or https {@link Uri}s. * * @deprecated Use {@link UrlUriLoader} instead */ @Deprecated public class HttpUriLoader extends UrlUriLoader { // Public API. @SuppressWarnings("WeakerAccess") public HttpUriLoader(ModelLoader urlLoader) { super(urlLoader); } /** * Factory for loading {@link InputStream}s from http/https {@link Uri}s. * * @deprecated Use {@link UrlUriLoader.StreamFactory} instead */ @Deprecated public static class Factory extends StreamFactory { // Defer to StreamFactory's implementation } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/stream/MediaStoreImageThumbLoader.java ================================================ package com.bumptech.glide.load.model.stream; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import com.bumptech.glide.load.data.mediastore.ThumbFetcher; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.signature.ObjectKey; import java.io.InputStream; /** * Loads {@link InputStream}s from media store image {@link Uri}s that point to pre-generated * thumbnails for those {@link Uri}s in the media store. */ public class MediaStoreImageThumbLoader implements ModelLoader { private final Context context; // Public API. @SuppressWarnings("WeakerAccess") public MediaStoreImageThumbLoader(Context context) { this.context = context.getApplicationContext(); } @Override public LoadData buildLoadData( @NonNull Uri model, int width, int height, @NonNull Options options) { if (MediaStoreUtil.isThumbnailSize(width, height)) { return new LoadData<>(new ObjectKey(model), ThumbFetcher.buildImageFetcher(context, model)); } else { return null; } } @Override public boolean handles(@NonNull Uri model) { return MediaStoreUtil.isMediaStoreImageUri(model); } /** Factory that loads {@link InputStream}s from media store image {@link Uri}s. */ public static class Factory implements ModelLoaderFactory { private final Context context; public Factory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new MediaStoreImageThumbLoader(context); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/stream/MediaStoreVideoThumbLoader.java ================================================ package com.bumptech.glide.load.model.stream; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import com.bumptech.glide.load.data.mediastore.ThumbFetcher; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.load.resource.bitmap.VideoDecoder; import com.bumptech.glide.signature.ObjectKey; import java.io.InputStream; /** * Loads {@link InputStream}s from media store video {@link Uri}s that point to pre-generated * thumbnails for those {@link Uri}s in the media store. * *

    If {@link VideoDecoder#TARGET_FRAME} is set with a non-null value that is not equal to {@link * VideoDecoder#DEFAULT_FRAME}, this loader will always return {@code null}. The media store does * not use a defined frame to generate the thumbnail, so we cannot accurately fulfill requests for * specific frames. */ public class MediaStoreVideoThumbLoader implements ModelLoader { private final Context context; // Public API. @SuppressWarnings("WeakerAccess") public MediaStoreVideoThumbLoader(Context context) { this.context = context.getApplicationContext(); } @Override @Nullable public LoadData buildLoadData( @NonNull Uri model, int width, int height, @NonNull Options options) { if (MediaStoreUtil.isThumbnailSize(width, height) && isRequestingDefaultFrame(options)) { return new LoadData<>(new ObjectKey(model), ThumbFetcher.buildVideoFetcher(context, model)); } else { return null; } } private boolean isRequestingDefaultFrame(Options options) { Long specifiedFrame = options.get(VideoDecoder.TARGET_FRAME); return specifiedFrame != null && specifiedFrame == VideoDecoder.DEFAULT_FRAME; } @Override public boolean handles(@NonNull Uri model) { return MediaStoreUtil.isMediaStoreVideoUri(model); } /** * Loads {@link InputStream}s from media store image {@link Uri}s that point to pre-generated * thumbnails for those {@link Uri}s in the media store. */ public static class Factory implements ModelLoaderFactory { private final Context context; public Factory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new MediaStoreVideoThumbLoader(context); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/stream/QMediaStoreUriLoader.java ================================================ package com.bumptech.glide.load.model.stream; import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.util.Synthetic; import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; /** * Best effort attempt to work around various Q storage states and bugs. * *

    In particular, HEIC images on Q cannot be decoded if they've gone through Android's exif * redaction, due to a bug in the implementation that corrupts the file. To avoid the issue, we need * to get at the un-redacted File. There are two ways we can do so: * *

      *
    • MediaStore.setRequireOriginal *
    • Querying for and opening the file via the underlying file path, rather than via {@code * ContentResolver} *
    * *

    MediaStore.setRequireOriginal will only work for applications that target Q and request and * currently have {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}. It's the simplest * change to make, but it covers the fewest applications. * *

    Querying for the file path and opening the file directly works for applications that do not * target Q and for applications that do target Q but that opt in to legacy storage mode. Other * options are theoretically available for applications that do not target Q, but due to other bugs, * the only consistent way to get unredacted files is via the file system. * *

    This class does not fix applications that target Q, do not opt in to legacy storage and that * don't have {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}. * *

    Avoid using this class directly, it may be removed in any future version of Glide. * * @param The type of data this loader will load ({@link InputStream}, {@link * ParcelFileDescriptor}). */ @RequiresApi(Build.VERSION_CODES.Q) public final class QMediaStoreUriLoader implements ModelLoader { private final Context context; private final ModelLoader fileDelegate; private final ModelLoader uriDelegate; private final Class dataClass; @SuppressWarnings("WeakerAccess") @Synthetic QMediaStoreUriLoader( Context context, ModelLoader fileDelegate, ModelLoader uriDelegate, Class dataClass) { this.context = context.getApplicationContext(); this.fileDelegate = fileDelegate; this.uriDelegate = uriDelegate; this.dataClass = dataClass; } @Override public LoadData buildLoadData( @NonNull Uri uri, int width, int height, @NonNull Options options) { return new LoadData<>( new ObjectKey(uri), new QMediaStoreUriFetcher<>( context, fileDelegate, uriDelegate, uri, width, height, options, dataClass)); } @Override public boolean handles(@NonNull Uri uri) { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && MediaStoreUtil.isMediaStoreUri(uri); } private static final class QMediaStoreUriFetcher implements DataFetcher { private static final String[] PROJECTION = new String[] {MediaStore.MediaColumns.DATA}; private final Context context; private final ModelLoader fileDelegate; private final ModelLoader uriDelegate; private final Uri uri; private final int width; private final int height; private final Options options; private final Class dataClass; private volatile boolean isCancelled; @Nullable private volatile DataFetcher delegate; QMediaStoreUriFetcher( Context context, ModelLoader fileDelegate, ModelLoader uriDelegate, Uri uri, int width, int height, Options options, Class dataClass) { this.context = context.getApplicationContext(); this.fileDelegate = fileDelegate; this.uriDelegate = uriDelegate; this.uri = uri; this.width = width; this.height = height; this.options = options; this.dataClass = dataClass; } @Override public void loadData( @NonNull Priority priority, @NonNull DataCallback callback) { try { DataFetcher local = buildDelegateFetcher(); if (local == null) { callback.onLoadFailed( new IllegalArgumentException("Failed to build fetcher for: " + uri)); return; } delegate = local; if (isCancelled) { cancel(); } else { local.loadData(priority, callback); } } catch (FileNotFoundException e) { callback.onLoadFailed(e); } } @Nullable private DataFetcher buildDelegateFetcher() throws FileNotFoundException { LoadData result = buildDelegateData(); return result != null ? result.fetcher : null; } @Nullable private LoadData buildDelegateData() throws FileNotFoundException { if (Environment.isExternalStorageLegacy()) { return fileDelegate.buildLoadData(queryForFilePath(uri), width, height, options); } else { // Android Picker uris have MediaStore authority and does not accept requireOriginal. if (MediaStoreUtil.isAndroidPickerUri(uri)) { return uriDelegate.buildLoadData(uri, width, height, options); } Uri toLoad = isAccessMediaLocationGranted() ? MediaStore.setRequireOriginal(uri) : uri; return uriDelegate.buildLoadData(toLoad, width, height, options); } } @Override public void cleanup() { DataFetcher local = delegate; if (local != null) { local.cleanup(); } } @Override public void cancel() { isCancelled = true; DataFetcher local = delegate; if (local != null) { local.cancel(); } } @NonNull @Override public Class getDataClass() { return dataClass; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } @NonNull private File queryForFilePath(Uri uri) throws FileNotFoundException { Cursor cursor = null; try { cursor = context .getContentResolver() .query( uri, PROJECTION, /* selection= */ null, /* selectionArgs= */ null, /* sortOrder= */ null); if (cursor == null || !cursor.moveToFirst()) { throw new FileNotFoundException("Failed to media store entry for: " + uri); } String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)); if (TextUtils.isEmpty(path)) { throw new FileNotFoundException("File path was empty in media store for: " + uri); } return new File(path); } finally { if (cursor != null) { cursor.close(); } } } private boolean isAccessMediaLocationGranted() { return context.checkSelfPermission(android.Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED; } } /** Factory for {@link InputStream}. */ @RequiresApi(Build.VERSION_CODES.Q) public static final class InputStreamFactory extends Factory { public InputStreamFactory(Context context) { super(context, InputStream.class); } } /** Factory for {@link ParcelFileDescriptor}. */ @RequiresApi(Build.VERSION_CODES.Q) public static final class FileDescriptorFactory extends Factory { public FileDescriptorFactory(Context context) { super(context, ParcelFileDescriptor.class); } } private abstract static class Factory implements ModelLoaderFactory { private final Context context; private final Class dataClass; Factory(Context context, Class dataClass) { this.context = context; this.dataClass = dataClass; } @NonNull @Override public final ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new QMediaStoreUriLoader<>( context, multiFactory.build(File.class, dataClass), multiFactory.build(Uri.class, dataClass), dataClass); } @Override public final void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/model/stream/UrlLoader.java ================================================ package com.bumptech.glide.load.model.stream; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import java.io.InputStream; import java.net.URL; /** * A wrapper class that translates {@link java.net.URL} objects into {@link * com.bumptech.glide.load.model.GlideUrl} objects and then uses the wrapped {@link * com.bumptech.glide.load.model.ModelLoader} for {@link com.bumptech.glide.load.model.GlideUrl}s to * load the data. */ public class UrlLoader implements ModelLoader { private final ModelLoader glideUrlLoader; // Public API. @SuppressWarnings("WeakerAccess") public UrlLoader(ModelLoader glideUrlLoader) { this.glideUrlLoader = glideUrlLoader; } @Override public LoadData buildLoadData( @NonNull URL model, int width, int height, @NonNull Options options) { return glideUrlLoader.buildLoadData(new GlideUrl(model), width, height, options); } @Override public boolean handles(@NonNull URL model) { return true; } /** Factory for loading {@link InputStream}s from {@link URL}s. */ public static class StreamFactory implements ModelLoaderFactory { @NonNull @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new UrlLoader(multiFactory.build(GlideUrl.class, InputStream.class)); } @Override public void teardown() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/DefaultOnHeaderDecodedListener.java ================================================ package com.bumptech.glide.load.resource; import android.graphics.ColorSpace; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.DecodeException; import android.graphics.ImageDecoder.ImageInfo; import android.graphics.ImageDecoder.OnHeaderDecodedListener; import android.graphics.ImageDecoder.OnPartialImageListener; import android.graphics.ImageDecoder.Source; import android.os.Build; import android.util.Log; import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.PreferredColorSpace; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.load.resource.bitmap.HardwareConfigState; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.Synthetic; /** * Downsamples, decodes, and rotates images according to their exif orientation using {@link * ImageDecoder}. * *

    Obeys all options in {@link Downsampler} except for {@link * Downsampler#FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS}. */ @RequiresApi(api = 28) public final class DefaultOnHeaderDecodedListener implements OnHeaderDecodedListener { private static final String TAG = "ImageDecoder"; @Synthetic private final HardwareConfigState hardwareConfigState = HardwareConfigState.getInstance(); private final int requestedWidth; private final int requestedHeight; private final DecodeFormat decodeFormat; private final DownsampleStrategy strategy; private final boolean isHardwareConfigAllowed; private final PreferredColorSpace preferredColorSpace; public DefaultOnHeaderDecodedListener( int requestedWidth, int requestedHeight, @NonNull Options options) { this.requestedWidth = requestedWidth; this.requestedHeight = requestedHeight; decodeFormat = options.get(Downsampler.DECODE_FORMAT); strategy = options.get(DownsampleStrategy.OPTION); isHardwareConfigAllowed = options.get(Downsampler.ALLOW_HARDWARE_CONFIG) != null && options.get(Downsampler.ALLOW_HARDWARE_CONFIG); preferredColorSpace = options.get(Downsampler.PREFERRED_COLOR_SPACE); } @Override public void onHeaderDecoded( @NonNull ImageDecoder decoder, @NonNull ImageInfo info, @NonNull Source source) { if (hardwareConfigState.isHardwareConfigAllowed( requestedWidth, requestedHeight, isHardwareConfigAllowed, /* isExifOrientationRequired= */ false)) { decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE); } else { decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); } if (decodeFormat == DecodeFormat.PREFER_RGB_565) { decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM); } decoder.setOnPartialImageListener( new OnPartialImageListener() { @Override public boolean onPartialImage(@NonNull DecodeException e) { // Never return partial images. return false; } }); Size size = info.getSize(); int targetWidth = requestedWidth; if (requestedWidth == Target.SIZE_ORIGINAL) { targetWidth = size.getWidth(); } int targetHeight = requestedHeight; if (requestedHeight == Target.SIZE_ORIGINAL) { targetHeight = size.getHeight(); } float scaleFactor = strategy.getScaleFactor(size.getWidth(), size.getHeight(), targetWidth, targetHeight); int resizeWidth = Math.round(scaleFactor * size.getWidth()); int resizeHeight = Math.round(scaleFactor * size.getHeight()); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Resizing" + " from [" + size.getWidth() + "x" + size.getHeight() + "]" + " to [" + resizeWidth + "x" + resizeHeight + "]" + " scaleFactor: " + scaleFactor); } decoder.setTargetSize(resizeWidth, resizeHeight); if (preferredColorSpace != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { boolean isP3Eligible = preferredColorSpace == PreferredColorSpace.DISPLAY_P3 && info.getColorSpace() != null && info.getColorSpace().isWideGamut(); decoder.setTargetColorSpace( ColorSpace.get(isP3Eligible ? ColorSpace.Named.DISPLAY_P3 : ColorSpace.Named.SRGB)); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)); } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/SimpleResource.java ================================================ package com.bumptech.glide.load.resource; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.Preconditions; /** * Simple wrapper for an arbitrary object which helps to satisfy some of the glide engine's * contracts. Suggested usages only include resource object which don't have size and cannot be * recycled/closed. * * @param type of the wrapped resource */ // TODO: there isn't much point in caching these... public class SimpleResource implements Resource { protected final T data; public SimpleResource(@NonNull T data) { this.data = Preconditions.checkNotNull(data); } @NonNull @SuppressWarnings("unchecked") @Override public Class getResourceClass() { return (Class) data.getClass(); } @NonNull @Override public final T get() { return data; } @Override public final int getSize() { return 1; } @Override public void recycle() { // no op } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/UnitTransformation.java ================================================ package com.bumptech.glide.load.resource; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import java.security.MessageDigest; /** * A no-op Transformation that simply returns the given resource. * * @param The type of the resource that will always be returned unmodified. */ public final class UnitTransformation implements Transformation { private static final Transformation TRANSFORMATION = new UnitTransformation<>(); /** * Returns a UnitTransformation for the given type. * * @param The type of the resource to be transformed. */ @SuppressWarnings("unchecked") @NonNull public static UnitTransformation get() { return (UnitTransformation) TRANSFORMATION; } private UnitTransformation() { // Only accessible as a singleton. } @NonNull @Override public Resource transform( @NonNull Context context, @NonNull Resource resource, int outWidth, int outHeight) { return resource; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { // Do nothing. } /* Use default implementations of equals and hashcode. */ } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapDrawableDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; import java.io.IOException; /** * Decodes an {@link android.graphics.drawable.BitmapDrawable} for a data type. * * @param The type of data that will be decoded. */ public class BitmapDrawableDecoder implements ResourceDecoder { private final ResourceDecoder decoder; private final Resources resources; // Public API. @SuppressWarnings({"unused", "WeakerAccess"}) public BitmapDrawableDecoder(Context context, ResourceDecoder decoder) { this(context.getResources(), decoder); } /** * @deprecated Use {@link #BitmapDrawableDecoder(Context, ResourceDecoder)}, {@code bitmapPool} is * ignored. */ @Deprecated public BitmapDrawableDecoder( Resources resources, @SuppressWarnings("unused") BitmapPool bitmapPool, ResourceDecoder decoder) { this(resources, decoder); } public BitmapDrawableDecoder( @NonNull Resources resources, @NonNull ResourceDecoder decoder) { this.resources = Preconditions.checkNotNull(resources); this.decoder = Preconditions.checkNotNull(decoder); } @Override public boolean handles(@NonNull DataType source, @NonNull Options options) throws IOException { return decoder.handles(source, options); } @Override public Resource decode( @NonNull DataType source, int width, int height, @NonNull Options options) throws IOException { Resource bitmapResource = decoder.decode(source, width, height, options); return LazyBitmapDrawableResource.obtain(resources, bitmapResource); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapDrawableEncoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.annotation.NonNull; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import java.io.File; /** Encodes {@link android.graphics.drawable.BitmapDrawable}s. */ public class BitmapDrawableEncoder implements ResourceEncoder { private final BitmapPool bitmapPool; private final ResourceEncoder encoder; public BitmapDrawableEncoder(BitmapPool bitmapPool, ResourceEncoder encoder) { this.bitmapPool = bitmapPool; this.encoder = encoder; } @Override public boolean encode( @NonNull Resource data, @NonNull File file, @NonNull Options options) { return encoder.encode(new BitmapResource(data.get().getBitmap(), bitmapPool), file, options); } @NonNull @Override public EncodeStrategy getEncodeStrategy(@NonNull Options options) { return encoder.getEncodeStrategy(options); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapDrawableResource.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.Initializable; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.drawable.DrawableResource; import com.bumptech.glide.util.Util; /** * A {@link com.bumptech.glide.load.engine.Resource} that wraps an {@link * android.graphics.drawable.BitmapDrawable} * *

    This class ensures that every call to {@link #get()}} always returns a new {@link * android.graphics.drawable.BitmapDrawable} to avoid rendering issues if used in multiple views and * is also responsible for returning the underlying {@link android.graphics.Bitmap} to the given * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} when the resource is recycled. */ public class BitmapDrawableResource extends DrawableResource implements Initializable { private final BitmapPool bitmapPool; // Public API. @SuppressWarnings("WeakerAccess") public BitmapDrawableResource(BitmapDrawable drawable, BitmapPool bitmapPool) { super(drawable); this.bitmapPool = bitmapPool; } @NonNull @Override public Class getResourceClass() { return BitmapDrawable.class; } @Override public int getSize() { return Util.getBitmapByteSize(drawable.getBitmap()); } @Override public void recycle() { bitmapPool.put(drawable.getBitmap()); } @Override public void initialize() { drawable.getBitmap().prepareToDraw(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapDrawableTransformation.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.Preconditions; import java.security.MessageDigest; /** * Transforms {@link android.graphics.drawable.BitmapDrawable}s. * * @deprecated Use {@link DrawableTransformation} instead. */ @Deprecated public class BitmapDrawableTransformation implements Transformation { private final Transformation wrapped; // Public API. @SuppressWarnings("WeakerAccess") public BitmapDrawableTransformation(Transformation wrapped) { this.wrapped = Preconditions.checkNotNull(new DrawableTransformation(wrapped, /* isRequired= */ false)); } @NonNull @Override public Resource transform( @NonNull Context context, @NonNull Resource drawableResourceToTransform, int outWidth, int outHeight) { Resource toTransform = convertToDrawableResource(drawableResourceToTransform); Resource transformed = wrapped.transform(context, toTransform, outWidth, outHeight); return convertToBitmapDrawableResource(transformed); } @SuppressWarnings("unchecked") private static Resource convertToBitmapDrawableResource( Resource resource) { if (!(resource.get() instanceof BitmapDrawable)) { throw new IllegalArgumentException( "Wrapped transformation unexpectedly returned a non BitmapDrawable resource: " + resource.get()); } return (Resource) (Resource) resource; } @SuppressWarnings("unchecked") private static Resource convertToDrawableResource(Resource toConvert) { return (Resource) (Resource) toConvert; } @SuppressWarnings("deprecation") @Override public boolean equals(Object o) { if (o instanceof BitmapDrawableTransformation) { BitmapDrawableTransformation other = (BitmapDrawableTransformation) o; return wrapped.equals(other.wrapped); } return false; } @Override public int hashCode() { return wrapped.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { wrapped.updateDiskCacheKey(messageDigest); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapEncoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.data.BufferedOutputStream; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Util; import com.bumptech.glide.util.pool.GlideTrace; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; /** * An {@link com.bumptech.glide.load.ResourceEncoder} that writes {@link android.graphics.Bitmap}s * to {@link java.io.OutputStream}s. * *

    {@link android.graphics.Bitmap}s that return true from {@link android.graphics.Bitmap#hasAlpha * ()}} are written using {@link android.graphics.Bitmap.CompressFormat#PNG} to preserve alpha and * all other bitmaps are written using {@link android.graphics.Bitmap.CompressFormat#JPEG}. * * @see android.graphics.Bitmap#compress(android.graphics.Bitmap.CompressFormat, int, * java.io.OutputStream) */ public class BitmapEncoder implements ResourceEncoder { /** * An integer option between 0 and 100 that is used as the compression quality. * *

    Defaults to 90. */ public static final Option COMPRESSION_QUALITY = Option.memory("com.bumptech.glide.load.resource.bitmap.BitmapEncoder.CompressionQuality", 90); /** * An {@link android.graphics.Bitmap.CompressFormat} option used as the format to encode the * {@link android.graphics.Bitmap}. * *

    Defaults to {@link android.graphics.Bitmap.CompressFormat#JPEG} for images without alpha and * {@link android.graphics.Bitmap.CompressFormat#PNG} for images with alpha. */ public static final Option COMPRESSION_FORMAT = Option.memory("com.bumptech.glide.load.resource.bitmap.BitmapEncoder.CompressionFormat"); private static final String TAG = "BitmapEncoder"; @Nullable private final ArrayPool arrayPool; public BitmapEncoder(@NonNull ArrayPool arrayPool) { this.arrayPool = arrayPool; } /** * @deprecated Use {@link #BitmapEncoder(ArrayPool)} instead. */ @Deprecated public BitmapEncoder() { arrayPool = null; } @Override public boolean encode( @NonNull Resource resource, @NonNull File file, @NonNull Options options) { final Bitmap bitmap = resource.get(); Bitmap.CompressFormat format = getFormat(bitmap, options); GlideTrace.beginSectionFormat( "encode: [%dx%d] %s", bitmap.getWidth(), bitmap.getHeight(), format); try { long start = LogTime.getLogTime(); int quality = options.get(COMPRESSION_QUALITY); boolean success = false; OutputStream os = null; try { os = new FileOutputStream(file); if (arrayPool != null) { os = new BufferedOutputStream(os, arrayPool); } bitmap.compress(format, quality, os); os.close(); success = true; } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to encode Bitmap", e); } } finally { if (os != null) { try { os.close(); } catch (IOException e) { // Do nothing. } } } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Compressed with type: " + format + " of size " + Util.getBitmapByteSize(bitmap) + " in " + LogTime.getElapsedMillis(start) + ", options format: " + options.get(COMPRESSION_FORMAT) + ", hasAlpha: " + bitmap.hasAlpha()); } return success; } finally { GlideTrace.endSection(); } } private Bitmap.CompressFormat getFormat(Bitmap bitmap, Options options) { Bitmap.CompressFormat format = options.get(COMPRESSION_FORMAT); if (format != null) { return format; } else if (bitmap.hasAlpha()) { return Bitmap.CompressFormat.PNG; } else { return Bitmap.CompressFormat.JPEG; } } @NonNull @Override public EncodeStrategy getEncodeStrategy(@NonNull Options options) { return EncodeStrategy.TRANSFORMED; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapImageDecoderResourceDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.Source; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.load.resource.DefaultOnHeaderDecodedListener; import java.io.IOException; /** {@link Bitmap} specific implementation of {@link DefaultOnHeaderDecodedListener}. */ @RequiresApi(api = 28) public final class BitmapImageDecoderResourceDecoder implements ResourceDecoder { private static final String TAG = "BitmapImageDecoder"; private final BitmapPool bitmapPool = new BitmapPoolAdapter(); @Override public boolean handles(@NonNull Source source, @NonNull Options options) throws IOException { return true; } @Override public Resource decode( @NonNull Source source, int width, int height, @NonNull Options options) throws IOException { Bitmap result = ImageDecoder.decodeBitmap( source, new DefaultOnHeaderDecodedListener(width, height, options)); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Decoded" + " [" + result.getWidth() + "x" + result.getHeight() + "]" + " for [" + width + "x" + height + "]"); } return new BitmapResource(result, bitmapPool); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapResource.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.engine.Initializable; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; /** A resource wrapping a {@link android.graphics.Bitmap} object. */ public class BitmapResource implements Resource, Initializable { private final Bitmap bitmap; private final BitmapPool bitmapPool; /** * Returns a new {@link BitmapResource} wrapping the given {@link Bitmap} if the Bitmap is * non-null or null if the given Bitmap is null. * * @param bitmap A Bitmap. * @param bitmapPool A non-null {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool}. */ @Nullable public static BitmapResource obtain(@Nullable Bitmap bitmap, @NonNull BitmapPool bitmapPool) { if (bitmap == null) { return null; } else { return new BitmapResource(bitmap, bitmapPool); } } public BitmapResource(@NonNull Bitmap bitmap, @NonNull BitmapPool bitmapPool) { this.bitmap = Preconditions.checkNotNull(bitmap, "Bitmap must not be null"); this.bitmapPool = Preconditions.checkNotNull(bitmapPool, "BitmapPool must not be null"); } @NonNull @Override public Class getResourceClass() { return Bitmap.class; } @NonNull @Override public Bitmap get() { return bitmap; } @Override public int getSize() { return Util.getBitmapByteSize(bitmap); } @Override public void recycle() { bitmapPool.put(bitmap); } @Override public void initialize() { bitmap.prepareToDraw(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapTransformation.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.content.Context; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.Util; import java.nio.charset.Charset; import java.security.MessageDigest; /** * A simple {@link com.bumptech.glide.load.Transformation} for transforming {@link * android.graphics.Bitmap}s that abstracts away dealing with {@link * com.bumptech.glide.load.engine.Resource} objects for subclasses. * *

    Use cases will look something like this: * *

    {@code
     * public class FillSpace extends BitmapTransformation {
     *     private static final String ID = "com.bumptech.glide.transformations.FillSpace";
     *     private static final byte[] ID_BYTES = ID.getBytes(Charset.forName("UTF-8"));
     *
     *     {@literal @Override}
     *     public Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
     *         if (toTransform.getWidth() == outWidth && toTransform.getHeight() == outHeight) {
     *             return toTransform;
     *         }
     *
     *         return Bitmap.createScaledBitmap(toTransform, outWidth, outHeight, true);
     *     }
     *
     *     {@literal @Override}
     *     public boolean equals(Object o) {
     *       return o instanceof FillSpace;
     *     }
     *
     *     {@literal @Override}
     *     public int hashCode() {
     *       return ID.hashCode();
     *     }
     *
     *     {@literal @Override}
     *     public void updateDiskCacheKey(MessageDigest messageDigest) {
     *       messageDigest.update(ID_BYTES);
     *     }
     * }
     * }
    * *

    Using the fully qualified class name as a static final {@link String} (not {@link * Class#getName()} to avoid proguard obfuscation) is an easy way to implement {@link * #updateDiskCacheKey(java.security.MessageDigest)}} correctly. If additional arguments are * required they can be passed in to the constructor of the {@code Transformation} and then used to * update the {@link java.security.MessageDigest} passed in to {@link * #updateDiskCacheKey(MessageDigest)}. If arguments are primitive types, they can typically easily * be serialized using {@link java.nio.ByteBuffer}. {@link String} types can be serialized with * {@link String#getBytes(Charset)} using the constant {@link #CHARSET}. * *

    As with all {@link Transformation}s, all subclasses must implement {@link * #equals(Object)} and {@link #hashCode()} for memory caching to work correctly. */ public abstract class BitmapTransformation implements Transformation { @NonNull @Override public final Resource transform( @NonNull Context context, @NonNull Resource resource, int outWidth, int outHeight) { if (!Util.isValidDimensions(outWidth, outHeight)) { throw new IllegalArgumentException( "Cannot apply transformation on width: " + outWidth + " or height: " + outHeight + " less than or equal to zero and not Target.SIZE_ORIGINAL"); } BitmapPool bitmapPool = Glide.get(context).getBitmapPool(); Bitmap toTransform = resource.get(); int targetWidth = outWidth == Target.SIZE_ORIGINAL ? toTransform.getWidth() : outWidth; int targetHeight = outHeight == Target.SIZE_ORIGINAL ? toTransform.getHeight() : outHeight; Bitmap transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight); final Resource result; if (toTransform.equals(transformed)) { result = resource; } else { result = BitmapResource.obtain(transformed, bitmapPool); } return result; } /** * Transforms the given {@link android.graphics.Bitmap} based on the given dimensions and returns * the transformed result. * *

    The provided Bitmap, toTransform, should not be recycled or returned to the pool. Glide will * automatically recycle and/or reuse toTransform if the transformation returns a different * Bitmap. Similarly implementations should never recycle or return Bitmaps that are returned as * the result of this method. Recycling or returning the provided and/or the returned Bitmap to * the pool will lead to a variety of runtime exceptions and drawing errors. See #408 for an * example. If the implementation obtains and discards intermediate Bitmaps, they may safely be * returned to the BitmapPool and/or recycled. * *

    outWidth and outHeight will never be {@link * com.bumptech.glide.request.target.Target#SIZE_ORIGINAL}, this class converts them to be the * size of the Bitmap we're going to transform before calling this method. * * @param pool A {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} that can be used * to obtain and return intermediate {@link Bitmap}s used in this transformation. For every * {@link android.graphics.Bitmap} obtained from the pool during this transformation, a {@link * android.graphics.Bitmap} must also be returned. * @param toTransform The {@link android.graphics.Bitmap} to transform. * @param outWidth The ideal width of the transformed bitmap (the transformed width does not need * to match exactly). * @param outHeight The ideal height of the transformed bitmap (the transformed height does not * need to match exactly). */ protected abstract Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/BitmapTransitionOptions.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.request.transition.BitmapTransitionFactory; import com.bumptech.glide.request.transition.DrawableCrossFadeFactory; import com.bumptech.glide.request.transition.TransitionFactory; /** Contains {@link Bitmap} specific animation options. */ // Public API. @SuppressWarnings({"unused", "WeakerAccess"}) public final class BitmapTransitionOptions extends TransitionOptions { /** * Returns a {@link BitmapTransitionOptions} object that enables a cross fade animation. * * @see #crossFade() */ @NonNull public static BitmapTransitionOptions withCrossFade() { return new BitmapTransitionOptions().crossFade(); } /** * Returns a {@link BitmapTransitionOptions} object that enables a cross fade animation. * * @see #crossFade(int) */ @NonNull public static BitmapTransitionOptions withCrossFade(int duration) { return new BitmapTransitionOptions().crossFade(duration); } /** * Returns a {@link BitmapTransitionOptions} object that enables a cross fade animation. * * @see #crossFade(DrawableCrossFadeFactory) */ @NonNull public static BitmapTransitionOptions withCrossFade( @NonNull DrawableCrossFadeFactory drawableCrossFadeFactory) { return new BitmapTransitionOptions().crossFade(drawableCrossFadeFactory); } /** * Returns a {@link BitmapTransitionOptions} object that enables a cross fade animation. * * @see #crossFade(DrawableCrossFadeFactory.Builder) */ @NonNull public static BitmapTransitionOptions withCrossFade( @NonNull DrawableCrossFadeFactory.Builder builder) { return new BitmapTransitionOptions().crossFade(builder); } /** * Returns a {@link BitmapTransitionOptions} object that enables a any animation that is possible * on drawables. * * @see #transitionUsing(TransitionFactory) */ @NonNull public static BitmapTransitionOptions withWrapped( @NonNull TransitionFactory drawableCrossFadeFactory) { return new BitmapTransitionOptions().transitionUsing(drawableCrossFadeFactory); } /** * Returns a {@link BitmapTransitionOptions} object that uses the given transition factory. * * @see com.bumptech.glide.GenericTransitionOptions#with(TransitionFactory) */ @NonNull public static BitmapTransitionOptions with(@NonNull TransitionFactory transitionFactory) { return new BitmapTransitionOptions().transition(transitionFactory); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). */ @NonNull public BitmapTransitionOptions crossFade() { return crossFade(new DrawableCrossFadeFactory.Builder()); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). * * @param duration The duration of the animation, see {@code * DrawableCrossFadeFactory.Builder(int)}. * @see com.bumptech.glide.request.transition.DrawableCrossFadeFactory.Builder */ @NonNull public BitmapTransitionOptions crossFade(int duration) { return crossFade(new DrawableCrossFadeFactory.Builder(duration)); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). */ @NonNull public BitmapTransitionOptions crossFade( @NonNull DrawableCrossFadeFactory drawableCrossFadeFactory) { return transitionUsing(drawableCrossFadeFactory); } /** Enables a any Drawable based animation to run on Bitmaps as well. */ @NonNull public BitmapTransitionOptions transitionUsing( @NonNull TransitionFactory drawableCrossFadeFactory) { return transition(new BitmapTransitionFactory(drawableCrossFadeFactory)); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). */ @NonNull public BitmapTransitionOptions crossFade(@NonNull DrawableCrossFadeFactory.Builder builder) { return transitionUsing(builder.build()); } // Make sure that we're not equal to any other concrete implementation of TransitionOptions. @Override public boolean equals(Object o) { return o instanceof BitmapTransitionOptions && super.equals(o); } // Our class doesn't include any additional properties, so we don't need to modify hashcode, but // keep it here as a reminder in case we add properties. @SuppressWarnings("PMD.UselessOverridingMethod") @Override public int hashCode() { return super.hashCode(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/ByteBufferBitmapDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import java.io.IOException; import java.nio.ByteBuffer; /** Decodes {@link android.graphics.Bitmap Bitmaps} from {@link java.nio.ByteBuffer ByteBuffers}. */ public class ByteBufferBitmapDecoder implements ResourceDecoder { private final Downsampler downsampler; public ByteBufferBitmapDecoder(Downsampler downsampler) { this.downsampler = downsampler; } @Override public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) { return downsampler.handles(source); } @Override public Resource decode( @NonNull ByteBuffer source, int width, int height, @NonNull Options options) throws IOException { return downsampler.decode(source, width, height, options); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/ByteBufferBitmapImageDecoderResourceDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.Source; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import java.io.IOException; import java.nio.ByteBuffer; /** * {@link ByteBuffer} specific implementation of {@link * ByteBufferBitmapImageDecoderResourceDecoder}. */ @RequiresApi(api = 28) public final class ByteBufferBitmapImageDecoderResourceDecoder implements ResourceDecoder { private final BitmapImageDecoderResourceDecoder wrapped = new BitmapImageDecoderResourceDecoder(); @Override public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException { return true; } @Override public Resource decode( @NonNull ByteBuffer buffer, int width, int height, @NonNull Options options) throws IOException { Source source = ImageDecoder.createSource(buffer); return wrapped.decode(source, width, height, options); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/CenterCrop.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import java.security.MessageDigest; /** * Scale the image so that either the width of the image matches the given width and the height of * the image is greater than the given height or vice versa, and then crop the larger dimension to * match the given dimension. * *

    Does not maintain the image's aspect ratio */ public class CenterCrop extends BitmapTransformation { private static final String ID = "com.bumptech.glide.load.resource.bitmap.CenterCrop"; private static final byte[] ID_BYTES = ID.getBytes(CHARSET); @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return TransformationUtils.centerCrop(pool, toTransform, outWidth, outHeight); } @Override public boolean equals(Object o) { return o instanceof CenterCrop; } @Override public int hashCode() { return ID.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(ID_BYTES); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/CenterInside.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import java.security.MessageDigest; /** * Returns the image with its original size if its dimensions match or are smaller than the * target's, couple with {@link android.widget.ImageView.ScaleType#CENTER_INSIDE} in order to center * it in Target. If not, then it is scaled so that one of the dimensions of the image will be equal * to the given dimension and the other will be less than the given dimension (maintaining the * image's aspect ratio). */ public class CenterInside extends BitmapTransformation { private static final String ID = "com.bumptech.glide.load.resource.bitmap.CenterInside"; private static final byte[] ID_BYTES = ID.getBytes(CHARSET); @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return TransformationUtils.centerInside(pool, toTransform, outWidth, outHeight); } @Override public boolean equals(Object o) { return o instanceof CenterInside; } @Override public int hashCode() { return ID.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(ID_BYTES); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/CircleCrop.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import java.security.MessageDigest; /** * A Glide {@link BitmapTransformation} to circle crop an image. Behaves similar to a {@link * FitCenter} transform, but the resulting image is masked to a circle. * *

    Uses a PorterDuff blend mode, see http://ssp.impulsetrain.com/porterduff.html. */ public class CircleCrop extends BitmapTransformation { // The version of this transformation, incremented to correct an error in a previous version. // See #455. private static final int VERSION = 1; private static final String ID = "com.bumptech.glide.load.resource.bitmap.CircleCrop." + VERSION; private static final byte[] ID_BYTES = ID.getBytes(CHARSET); // Bitmap doesn't implement equals, so == and .equals are equivalent here. @SuppressWarnings("PMD.CompareObjectsWithEquals") @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return TransformationUtils.circleCrop(pool, toTransform, outWidth, outHeight); } @Override public boolean equals(Object o) { return o instanceof CircleCrop; } @Override public int hashCode() { return ID.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(ID_BYTES); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParser.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.ANIMATED_AVIF; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.ANIMATED_WEBP; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.AVIF; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.GIF; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.JPEG; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.PNG; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.PNG_A; import static com.bumptech.glide.load.ImageHeaderParser.ImageType.UNKNOWN; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; /** A class for parsing the exif orientation and other data from an image header. */ public final class DefaultImageHeaderParser implements ImageHeaderParser { // Due to https://code.google.com/p/android/issues/detail?id=97751. // TAG needs to be under 23 chars, so "Default" > "Dflt". private static final String TAG = "DfltImageHeaderParser"; private static final int GIF_HEADER = 0x474946; private static final int PNG_HEADER = 0x89504E47; static final int EXIF_MAGIC_NUMBER = 0xFFD8; // "MM". private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D; // "II". private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949; private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0"; static final byte[] JPEG_EXIF_SEGMENT_PREAMBLE_BYTES = JPEG_EXIF_SEGMENT_PREAMBLE.getBytes(Charset.forName("UTF-8")); private static final String JPEG_MPF_SEGMENT_PREAMBLE = "MPF"; static final byte[] JPEG_MPF_SEGMENT_PREAMBLE_BYTES = JPEG_MPF_SEGMENT_PREAMBLE.getBytes(Charset.forName("UTF-8")); private static final int SEGMENT_SOS = 0xDA; private static final int MARKER_EOI = 0xD9; static final int SEGMENT_START_ID = 0xFF; static final int EXIF_SEGMENT_TYPE = 0xE1; static final int APP2_SEGMENT_TYPE = 0xE2; private static final int ORIENTATION_TAG_TYPE = 0x0112; private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8}; // WebP-related // "RIFF" private static final int RIFF_HEADER = 0x52494646; // "WEBP" private static final int WEBP_HEADER = 0x57454250; // "VP8" null. private static final int VP8_HEADER = 0x56503800; private static final int VP8_HEADER_MASK = 0xFFFFFF00; private static final int VP8_HEADER_TYPE_MASK = 0x000000FF; // 'X' private static final int VP8_HEADER_TYPE_EXTENDED = 0x00000058; // 'L' private static final int VP8_HEADER_TYPE_LOSSLESS = 0x0000004C; private static final int WEBP_EXTENDED_ANIMATION_FLAG = 1 << 1; private static final int WEBP_EXTENDED_ALPHA_FLAG = 1 << 4; private static final int WEBP_LOSSLESS_ALPHA_FLAG = 1 << 3; // Avif-related // "ftyp" private static final int FTYP_HEADER = 0x66747970; // "avif" private static final int AVIF_BRAND = 0x61766966; // "avis" private static final int AVIS_BRAND = 0x61766973; @NonNull @Override public ImageType getType(@NonNull InputStream is) throws IOException { return getType(new StreamReader(Preconditions.checkNotNull(is))); } @NonNull @Override public ImageType getType(@NonNull ByteBuffer byteBuffer) throws IOException { return getType(new ByteBufferReader(Preconditions.checkNotNull(byteBuffer))); } @Override public int getOrientation(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { return getOrientation( new StreamReader(Preconditions.checkNotNull(is)), Preconditions.checkNotNull(byteArrayPool)); } @Override public int getOrientation(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException { return getOrientation( new ByteBufferReader(Preconditions.checkNotNull(byteBuffer)), Preconditions.checkNotNull(byteArrayPool)); } @Override public boolean hasJpegMpf(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { return hasJpegMpf( new StreamReader(Preconditions.checkNotNull(is)), Preconditions.checkNotNull(byteArrayPool)); } @Override public boolean hasJpegMpf(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException { return hasJpegMpf( new ByteBufferReader(Preconditions.checkNotNull(byteBuffer)), Preconditions.checkNotNull(byteArrayPool)); } private boolean hasJpegMpf(@NonNull Reader reader, @NonNull ArrayPool byteArrayPool) throws IOException { if (getType(reader) != JPEG) { return false; } int app2SegmentLength = moveToApp2SegmentAndGetLength(reader); while (app2SegmentLength > 0) { byte[] app2Data = byteArrayPool.get(app2SegmentLength, byte[].class); try { boolean hasJpegMpfPreamble = hasJpegMpfPreamble(reader, app2Data, app2SegmentLength); if (hasJpegMpfPreamble) { return true; } } finally { byteArrayPool.put(app2Data); } app2SegmentLength = moveToApp2SegmentAndGetLength(reader); } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "hasMpf: Failed to parse APP2 segment length, or no APP2 segment with MPF metadata not" + " found"); } return false; } @NonNull private ImageType getType(Reader reader) throws IOException { try { final int firstTwoBytes = reader.getUInt16(); // JPEG. if (firstTwoBytes == EXIF_MAGIC_NUMBER) { return JPEG; } final int firstThreeBytes = (firstTwoBytes << 8) | reader.getUInt8(); if (firstThreeBytes == GIF_HEADER) { return GIF; } final int firstFourBytes = (firstThreeBytes << 8) | reader.getUInt8(); // PNG. if (firstFourBytes == PNG_HEADER) { // See: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha // -color-type reader.skip(25 - 4); try { int alpha = reader.getUInt8(); // A RGB indexed PNG can also have transparency. Better safe than sorry! return alpha >= 3 ? PNG_A : PNG; } catch (Reader.EndOfFileException e) { // TODO(b/143917798): Re-enable this logging when dependent tests are fixed. // if (Log.isLoggable(TAG, Log.ERROR)) { // Log.e(TAG, "Unexpected EOF, assuming no alpha", e); // } return PNG; } } if (firstFourBytes != RIFF_HEADER) { // Check for AVIF (reads up to 32 bytes). If it is a valid AVIF stream, then the // firstFourBytes will be the size of the FTYP box. return sniffAvif(reader, /* boxSize= */ firstFourBytes); } // WebP (reads up to 21 bytes). // See https://developers.google.com/speed/webp/docs/riff_container for details. // Bytes 4 - 7 contain length information. Skip these. reader.skip(4); final int thirdFourBytes = (reader.getUInt16() << 16) | reader.getUInt16(); if (thirdFourBytes != WEBP_HEADER) { return UNKNOWN; } final int fourthFourBytes = (reader.getUInt16() << 16) | reader.getUInt16(); if ((fourthFourBytes & VP8_HEADER_MASK) != VP8_HEADER) { return UNKNOWN; } if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_EXTENDED) { // Skip some more length bytes and check for transparency/alpha flag. reader.skip(4); short flags = reader.getUInt8(); if ((flags & WEBP_EXTENDED_ANIMATION_FLAG) != 0) { return ANIMATED_WEBP; } else if ((flags & WEBP_EXTENDED_ALPHA_FLAG) != 0) { return ImageType.WEBP_A; } else { return ImageType.WEBP; } } if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_LOSSLESS) { // See chromium.googlesource.com/webm/libwebp/+/master/doc/webp-lossless-bitstream-spec.txt // for more info. reader.skip(4); short flags = reader.getUInt8(); return (flags & WEBP_LOSSLESS_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP; } return ImageType.WEBP; } catch (Reader.EndOfFileException e) { // TODO(b/143917798): Re-enable this logging when dependent tests are fixed. // if (Log.isLoggable(TAG, Log.ERROR)) { // Log.e(TAG, "Unexpected EOF", e); // } return UNKNOWN; } } /** * Check if the bits look like an AVIF Image. AVIF Specification: * https://aomediacodec.github.io/av1-avif/ * * @return AVIF or ANIMATED_AVIF if the first few bytes look like it could be an AVIF Image or an * animated AVIF Image respectively, UNKNOWN otherwise. */ private ImageType sniffAvif(Reader reader, int boxSize) throws IOException { int chunkType = (reader.getUInt16() << 16) | reader.getUInt16(); if (chunkType != FTYP_HEADER) { return UNKNOWN; } // majorBrand. int brand = (reader.getUInt16() << 16) | reader.getUInt16(); // The overall logic is that, if any of the brands are 'avis', then we can conclude immediately // that it is an animated AVIF image. Otherwise, we conclude after seeing all the brands that if // one of them is 'avif', the it is a still AVIF image. if (brand == AVIS_BRAND) { return ANIMATED_AVIF; } boolean avifBrandSeen = brand == AVIF_BRAND; // Skip the minor version. reader.skip(4); // Check the first five minor brands. While there could theoretically be more than five minor // brands, it is rare in practice. This way we stop the loop from running several times on a // blob that just happened to look like an ftyp box. int sizeRemaining = boxSize - 16; if (sizeRemaining % 4 == 0) { for (int i = 0; i < 5 && sizeRemaining > 0; ++i, sizeRemaining -= 4) { brand = (reader.getUInt16() << 16) | reader.getUInt16(); if (brand == AVIS_BRAND) { return ANIMATED_AVIF; } else if (brand == AVIF_BRAND) { avifBrandSeen = true; } } } return avifBrandSeen ? AVIF : UNKNOWN; } /** * Parse the orientation from the image header. If it doesn't handle this image type (or this is * not an image) it will return a default value rather than throwing an exception. * * @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't * contain an orientation */ private int getOrientation(Reader reader, ArrayPool byteArrayPool) throws IOException { try { final int magicNumber = reader.getUInt16(); if (!handles(magicNumber)) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Parser doesn't handle magic number: " + magicNumber); } return UNKNOWN_ORIENTATION; } else { int exifSegmentLength = moveToExifSegmentAndGetLength(reader); if (exifSegmentLength == -1) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to parse exif segment length, or exif segment not found"); } return UNKNOWN_ORIENTATION; } byte[] exifData = byteArrayPool.get(exifSegmentLength, byte[].class); try { return parseExifSegment(reader, exifData, exifSegmentLength); } finally { byteArrayPool.put(exifData); } } } catch (Reader.EndOfFileException e) { // TODO(b/143917798): Re-enable this logging when dependent tests are fixed. // if (Log.isLoggable(TAG, Log.ERROR)) { // Log.e(TAG, "Unexpected EOF", e); // } return UNKNOWN_ORIENTATION; } } private int parseExifSegment(Reader reader, byte[] tempArray, int exifSegmentLength) throws IOException { int read = reader.read(tempArray, exifSegmentLength); if (read != exifSegmentLength) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Unable to read exif segment data" + ", length: " + exifSegmentLength + ", actually read: " + read); } return UNKNOWN_ORIENTATION; } boolean hasJpegExifPreamble = hasJpegExifPreamble(tempArray, exifSegmentLength); if (hasJpegExifPreamble) { return parseExifSegment(new RandomAccessReader(tempArray, exifSegmentLength)); } else { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Missing jpeg exif preamble"); } return UNKNOWN_ORIENTATION; } } private boolean hasJpegExifPreamble(byte[] exifData, int exifSegmentLength) { return hasMatchingBytes(exifData, exifSegmentLength, JPEG_EXIF_SEGMENT_PREAMBLE_BYTES); } private boolean hasMatchingBytes(byte[] bytes, int byteLength, byte[] bytesToMatch) { boolean result = bytes != null && bytesToMatch != null && byteLength > bytesToMatch.length; if (result) { for (int i = 0; i < bytesToMatch.length; i++) { if (bytes[i] != bytesToMatch[i]) { result = false; break; } } } return result; } /** * Moves reader to the start of the exif segment and returns the length of the exif segment or * {@code -1} if no exif segment is found. */ private int moveToExifSegmentAndGetLength(Reader reader) throws IOException { return moveToSegmentAndGetLength(reader, EXIF_SEGMENT_TYPE); } /** * Returns whether the reader, set at the beginning of the APP2 segment past the length bytes, * contains multi-picture format (MPF) data. * * @param reader must be set at the start of an APP2 segment, past the APP2 label and length * bytes. * @param tempArray for storing temporary array. Must be at least the size of {@code * app2SegmentLength}. * @param app2SegmentLength the length of the APP2 segment. * @throws IOException if an EOF is reached before anything was read. */ private boolean hasJpegMpfPreamble(Reader reader, byte[] tempArray, int app2SegmentLength) throws IOException { int read = reader.read(tempArray, app2SegmentLength); if (read != app2SegmentLength) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Unable to read APP2 segment data" + ", length: " + app2SegmentLength + ", actually read: " + read); } return false; } return hasMatchingBytes(tempArray, app2SegmentLength, JPEG_MPF_SEGMENT_PREAMBLE_BYTES); } private int moveToApp2SegmentAndGetLength(Reader reader) throws IOException { return moveToSegmentAndGetLength(reader, APP2_SEGMENT_TYPE); } /** * Moves reader to the start of the segment identified by the segment type (e.g., "0xE1" for APP1 * and returns the length of the exif segment or {@code -1} if no segment of that type is found. */ private int moveToSegmentAndGetLength(Reader reader, int requestedSegmentType) throws IOException { while (true) { short segmentId = reader.getUInt8(); if (segmentId != SEGMENT_START_ID) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Unknown segmentId=" + segmentId); } return -1; } short segmentType = reader.getUInt8(); if (segmentType == SEGMENT_SOS) { return -1; } else if (segmentType == MARKER_EOI) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Found MARKER_EOI in " + requestedSegmentType + " segment"); } return -1; } int segmentLength = reader.getUInt16(); // A segment includes the bytes that specify its length. int segmentContentsLength = segmentLength - 2; if (segmentType != requestedSegmentType) { long skipped = reader.skip(segmentContentsLength); if (skipped != segmentContentsLength) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Unable to skip enough data" + ", type: " + segmentType + ", wanted to skip: " + segmentContentsLength + ", but actually skipped: " + skipped); } return -1; } } else { return segmentContentsLength; } } } private static int parseExifSegment(RandomAccessReader segmentData) { final int headerOffsetSize = JPEG_EXIF_SEGMENT_PREAMBLE.length(); short byteOrderIdentifier = segmentData.getInt16(headerOffsetSize); final ByteOrder byteOrder; switch (byteOrderIdentifier) { case MOTOROLA_TIFF_MAGIC_NUMBER: byteOrder = ByteOrder.BIG_ENDIAN; break; case INTEL_TIFF_MAGIC_NUMBER: byteOrder = ByteOrder.LITTLE_ENDIAN; break; default: if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Unknown endianness = " + byteOrderIdentifier); } byteOrder = ByteOrder.BIG_ENDIAN; break; } segmentData.order(byteOrder); int firstIfdOffset = segmentData.getInt32(headerOffsetSize + 4) + headerOffsetSize; int tagCount = segmentData.getInt16(firstIfdOffset); for (int i = 0; i < tagCount; i++) { final int tagOffset = calcTagOffset(firstIfdOffset, i); final int tagType = segmentData.getInt16(tagOffset); // We only want orientation. if (tagType != ORIENTATION_TAG_TYPE) { continue; } final int formatCode = segmentData.getInt16(tagOffset + 2); // 12 is max format code. if (formatCode < 1 || formatCode > 12) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Got invalid format code = " + formatCode); } continue; } final int componentCount = segmentData.getInt32(tagOffset + 4); if (componentCount < 0) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Negative tiff component count"); } continue; } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Got tagIndex=" + i + " tagType=" + tagType + " formatCode=" + formatCode + " componentCount=" + componentCount); } final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode]; if (byteCount > 4) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Got byte count > 4, not orientation, continuing, formatCode=" + formatCode); } continue; } final int tagValueOffset = tagOffset + 8; if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Illegal tagValueOffset=" + tagValueOffset + " tagType=" + tagType); } continue; } if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Illegal number of bytes for TI tag data tagType=" + tagType); } continue; } // assume componentCount == 1 && fmtCode == 3 return segmentData.getInt16(tagValueOffset); } return -1; } private static int calcTagOffset(int ifdOffset, int tagIndex) { return ifdOffset + 2 + 12 * tagIndex; } private static boolean handles(int imageMagicNumber) { return (imageMagicNumber & EXIF_MAGIC_NUMBER) == EXIF_MAGIC_NUMBER || imageMagicNumber == MOTOROLA_TIFF_MAGIC_NUMBER || imageMagicNumber == INTEL_TIFF_MAGIC_NUMBER; } private static final class RandomAccessReader { private final ByteBuffer data; RandomAccessReader(byte[] data, int length) { this.data = (ByteBuffer) ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN).limit(length); } void order(ByteOrder byteOrder) { this.data.order(byteOrder); } int length() { return data.remaining(); } int getInt32(int offset) { return isAvailable(offset, 4) ? data.getInt(offset) : -1; } short getInt16(int offset) { return isAvailable(offset, 2) ? data.getShort(offset) : -1; } private boolean isAvailable(int offset, int byteSize) { return data.remaining() - offset >= byteSize; } } private interface Reader { /** * Reads and returns a 8-bit unsigned integer. * *

    Throws an {@link EndOfFileException} if an EOF is reached. */ short getUInt8() throws IOException; /** * Reads and returns a 16-bit unsigned integer. * *

    Throws an {@link EndOfFileException} if an EOF is reached. */ int getUInt16() throws IOException; /** * Reads and returns a byte array. * *

    Throws an {@link EndOfFileException} if an EOF is reached before anything was read. */ int read(byte[] buffer, int byteCount) throws IOException; long skip(long total) throws IOException; // TODO(timurrrr): Stop inheriting from IOException, and make sure all attempts to read from // a Reader correctly handle EOFs. final class EndOfFileException extends IOException { private static final long serialVersionUID = 1L; EndOfFileException() { super("Unexpectedly reached end of a file"); } } } private static final class ByteBufferReader implements Reader { private final ByteBuffer byteBuffer; ByteBufferReader(ByteBuffer byteBuffer) { this.byteBuffer = byteBuffer; byteBuffer.order(ByteOrder.BIG_ENDIAN); } @Override public short getUInt8() throws EndOfFileException { if (byteBuffer.remaining() < 1) { throw new EndOfFileException(); } return (short) (byteBuffer.get() & 0xFF); } @Override public int getUInt16() throws EndOfFileException { return ((int) getUInt8() << 8) | getUInt8(); } @Override public int read(byte[] buffer, int byteCount) { int toRead = Math.min(byteCount, byteBuffer.remaining()); if (toRead == 0) { return -1; } byteBuffer.get(buffer, 0 /*dstOffset*/, toRead); return toRead; } @Override public long skip(long total) { int toSkip = (int) Math.min(byteBuffer.remaining(), total); byteBuffer.position(byteBuffer.position() + toSkip); return toSkip; } } private static final class StreamReader implements Reader { private final InputStream is; // Motorola / big endian byte order. StreamReader(InputStream is) { this.is = is; } @Override public short getUInt8() throws IOException { int readResult = is.read(); if (readResult == -1) { throw new EndOfFileException(); } return (short) readResult; } @Override public int getUInt16() throws IOException { return ((int) getUInt8() << 8) | getUInt8(); } @Override public int read(byte[] buffer, int byteCount) throws IOException { int numBytesRead = 0; int lastReadResult = 0; while (numBytesRead < byteCount && (lastReadResult = is.read(buffer, numBytesRead, byteCount - numBytesRead)) != -1) { numBytesRead += lastReadResult; } if (numBytesRead == 0 && lastReadResult == -1) { throw new EndOfFileException(); } return numBytesRead; } @Override public long skip(long total) throws IOException { if (total < 0) { return 0; } long toSkip = total; while (toSkip > 0) { long skipped = is.skip(toSkip); if (skipped > 0) { toSkip -= skipped; } else { // Skip has no specific contract as to what happens when you reach the end of // the stream. To differentiate between temporarily not having more data and // having finished the stream, we read a single byte when we fail to skip any // amount of data. int testEofByte = is.read(); if (testEofByte == -1) { break; } else { toSkip--; } } } return total - toSkip; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategy.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.os.Build; import com.bumptech.glide.load.Option; import com.bumptech.glide.util.Synthetic; /** * Indicates the algorithm to use when downsampling images. * *

    {@code DownsampleStrategy} does not provide any guarantees about output sizes. Behavior will * differ depending on the {@link com.bumptech.glide.load.ResourceDecoder} using the strategy and * the version of Android the code runs on. Use {@code DownsampleStrategy} as an optimization to * improve memory efficiency only. If you need a particular size or shape output, use an {@link * com.bumptech.glide.load.Transformation} either instead or in addition to a {@code * DownsampleStrategy}. * *

    Some differences between versions of Android and {@link * com.bumptech.glide.load.ResourceDecoder}s are listed below, but the list is not comprehensive * because {@link DownsampleStrategy} only controls its output scale value, not how that output * value is used. * *

    On some versions of Android, precise scaling is not possible. In those cases, the strategies * can only pick between downsampling to between 1x the requested size and 2x the requested size and * between 0.5x the requested size and 1x the requested size because only power of two downsampling * is supported. To preserve the potential for a {@link com.bumptech.glide.load.Transformation} to * scale precisely without a loss in quality, all but {@link #AT_MOST} will prefer to downsample to * between 1x and 2x the requested size. */ // Public API. @SuppressWarnings("WeakerAccess") public abstract class DownsampleStrategy { /** * Downsamples so the image's smallest dimension is between the given dimensions and 2x the given * dimensions, with no size restrictions on the image's largest dimension. * *

    Does not upscale if the requested dimensions are larger than the original dimensions. */ public static final DownsampleStrategy AT_LEAST = new AtLeast(); /** * Downsamples so the image's largest dimension is between 1/2 the given dimensions and the given * dimensions, with no restrictions on the image's smallest dimension. * *

    Does not upscale if the requested dimensions are larger than the original dimensions. */ public static final DownsampleStrategy AT_MOST = new AtMost(); /** * Scales, maintaining the original aspect ratio, so that one of the image's dimensions is exactly * equal to the requested size and the other dimension is less than or equal to the requested * size. * *

    This method will upscale if the requested width and height are greater than the source width * and height. To avoid upscaling, use {@link #AT_LEAST}, {@link #AT_MOST} or {@link * #CENTER_INSIDE}. * *

    On pre-KitKat devices, {@code FIT_CENTER} will downsample by a power of two only so that one * of the image's dimensions is greater than or equal to the requested size. No guarantees are * made about the second dimensions. This is NOT the same as {@link #AT_LEAST} because * only one dimension, not both, are greater than or equal to the requested dimensions, the other * may be smaller. */ public static final DownsampleStrategy FIT_CENTER = new FitCenter(); /** Identical to {@link #FIT_CENTER}, but never upscales. */ public static final DownsampleStrategy CENTER_INSIDE = new CenterInside(); /** * Scales, maintaining the original aspect ratio, so that one of the image's dimensions is exactly * equal to the requested size and the other dimension is greater than or equal to the requested * size. * *

    This method will upscale if the requested width and height are greater than the source width * and height. To avoid upscaling, use {@link #AT_LEAST}, {@link #AT_MOST}, or {@link * #CENTER_INSIDE}. * *

    On pre-KitKat devices, {@link Downsampler} treats this as equivalent to {@link #AT_LEAST} * because only power of two downsampling can be used. */ public static final DownsampleStrategy CENTER_OUTSIDE = new CenterOutside(); /** Performs no downsampling or scaling. */ public static final DownsampleStrategy NONE = new None(); /** Default strategy, currently {@link #CENTER_OUTSIDE}. */ public static final DownsampleStrategy DEFAULT = CENTER_OUTSIDE; /** * Indicates the {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option that * will be used to calculate the sample size to use to downsample an image given the original and * target dimensions of the image. */ // The exact String value here is retained to avoid breaking cache keys for images that were // loaded with older versions of Glide. public static final Option OPTION = Option.memory( "com.bumptech.glide.load.resource.bitmap.Downsampler.DownsampleStrategy", DEFAULT); @Synthetic static final boolean IS_BITMAP_FACTORY_SCALING_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; /** * Returns a float (0, +infinity) indicating a scale factor to apply to the source width and * height when displayed in the requested width and height. * *

    The returned scale factor will be split into a power of two sample size applied via {@link * android.graphics.BitmapFactory.Options#inSampleSize} and a float scale factor applied after * downsampling via {@link android.graphics.BitmapFactory.Options#inTargetDensity} and {@link * android.graphics.BitmapFactory.Options#inDensity}. Because of rounding errors the scale factor * may not be applied precisely. * *

    The float scaling factor will only be applied on KitKat+. Prior to KitKat, only the power of * two downsampling will be applied. * * @param sourceWidth The width in pixels of the image to be downsampled. * @param sourceHeight The height in pixels of the image to be downsampled. * @param requestedWidth The width in pixels of the view/target the image will be displayed in. * @param requestedHeight The height in pixels of the view/target the image will be displayed in. */ public abstract float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight); /** * Returns a non-null {@link SampleSizeRounding} to use to resolve rounding errors and conflicts * between scaling for the width and the height of the image. * * @param sourceWidth The width in pixels of the image to be downsampled. * @param sourceHeight The height in pixels of the image to be downsampled. * @param requestedWidth The width in pixels of the view/target the image will be displayed in. * @param requestedHeight The height in pixels of the view/target the image will be displayed in. */ public abstract SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight); private static class FitCenter extends DownsampleStrategy { @Synthetic FitCenter() {} @Override public float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) { float widthPercentage = requestedWidth / (float) sourceWidth; float heightPercentage = requestedHeight / (float) sourceHeight; return Math.min(widthPercentage, heightPercentage); } else { // Similar to AT_LEAST, but only require one dimension or the other to be >= requested // rather than both. int maxIntegerFactor = Math.max(sourceHeight / requestedHeight, sourceWidth / requestedWidth); return maxIntegerFactor == 0 ? 1f : 1f / Integer.highestOneBit(maxIntegerFactor); } } @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) { return SampleSizeRounding.QUALITY; } else { // TODO: This doesn't seem right, but otherwise we can skip a sample size because QUALITY // prefers the smaller of the width and height scale factor. MEMORY is a hack that // lets us prefer the larger of the two. return SampleSizeRounding.MEMORY; } } } private static class CenterOutside extends DownsampleStrategy { @Synthetic CenterOutside() {} @Override public float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { float widthPercentage = requestedWidth / (float) sourceWidth; float heightPercentage = requestedHeight / (float) sourceHeight; return Math.max(widthPercentage, heightPercentage); } @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return SampleSizeRounding.QUALITY; } } private static class AtLeast extends DownsampleStrategy { @Synthetic AtLeast() {} @Override public float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { int minIntegerFactor = Math.min(sourceHeight / requestedHeight, sourceWidth / requestedWidth); return minIntegerFactor == 0 ? 1f : 1f / Integer.highestOneBit(minIntegerFactor); } @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return SampleSizeRounding.QUALITY; } } private static class AtMost extends DownsampleStrategy { @Synthetic AtMost() {} @Override public float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { int maxIntegerFactor = (int) Math.ceil( Math.max( sourceHeight / (float) requestedHeight, sourceWidth / (float) requestedWidth)); int lesserOrEqualSampleSize = Math.max(1, Integer.highestOneBit(maxIntegerFactor)); int greaterOrEqualSampleSize = lesserOrEqualSampleSize << (lesserOrEqualSampleSize < maxIntegerFactor ? 1 : 0); return 1f / greaterOrEqualSampleSize; } @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return SampleSizeRounding.MEMORY; } } private static class None extends DownsampleStrategy { @Synthetic None() {} @Override public float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return 1f; } @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return SampleSizeRounding.QUALITY; } } private static class CenterInside extends DownsampleStrategy { @Synthetic CenterInside() {} @Override public float getScaleFactor( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return Math.min( 1.f, FIT_CENTER.getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight)); } @Override public SampleSizeRounding getSampleSizeRounding( int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) { return getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight) == 1.f ? SampleSizeRounding.QUALITY : FIT_CENTER.getSampleSizeRounding( sourceWidth, sourceHeight, requestedWidth, requestedHeight); } } /** * Indicates whether to prefer to prefer downsampling or scaling to prefer lower memory usage or * higher quality. */ public enum SampleSizeRounding { /** * Prefer to round the sample size up so that the image is downsampled to smaller than the * requested size to use less memory. */ MEMORY, /** * Prefer to round the sample size down so that the image is downsampled to larger than the * requested size to maintain quality at the expense of extra memory usage. */ QUALITY, } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.ColorSpace; import android.os.Build; import android.os.ParcelFileDescriptor; import android.util.DisplayMetrics; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.PreferredColorSpace; import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.SampleSizeRounding; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.Set; /** * Downsamples, decodes, and rotates images according to their exif orientation using {@link * BitmapFactory}. */ public final class Downsampler { static final String TAG = "Downsampler"; /** * Indicates the {@link com.bumptech.glide.load.DecodeFormat} that will be used in conjunction * with the image format to determine the {@link android.graphics.Bitmap.Config} to provide to * {@link android.graphics.BitmapFactory.Options#inPreferredConfig} when decoding the image. */ public static final Option DECODE_FORMAT = Option.memory( "com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat", DecodeFormat.DEFAULT); /** * Sets the {@link PreferredColorSpace} that will be used along with the version of Android and * color space of the requested image to determine the final color space used to decode the image. * *

    Refer to {@link PreferredColorSpace} for details on how this option works and its various * limitations. */ public static final Option PREFERRED_COLOR_SPACE = Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.PreferredColorSpace"); /** * Indicates the {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option that * will be used to calculate the sample size to use to downsample an image given the original and * target dimensions of the image. * * @deprecated Use {@link DownsampleStrategy#OPTION} directly instead. */ @Deprecated public static final Option DOWNSAMPLE_STRATEGY = DownsampleStrategy.OPTION; /** * Ensure that the size of the bitmap is fixed to the requested width and height of the resource * from the caller. The final resource dimensions may differ from the requested width and height, * and thus setting this to true may result in the bitmap size differing from the resource * dimensions. * *

    This can be used as a performance optimization for KitKat and above by fixing the size of * the bitmap for a collection of requested resources so that the bitmap pool will not need to * allocate new bitmaps for images of different sizes. */ // Public API @SuppressWarnings("WeakerAccess") public static final Option FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS = Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.FixBitmapSize", false); /** * Indicates that it's safe or unsafe to decode {@link Bitmap}s with {@link * Bitmap.Config#HARDWARE}. * *

    Callers should almost never set this value to {@code true} manually. Glide will already do * so when Glide believes it's safe to do (when no transformations are applied). Instead, callers * can set this value to {@code false} to prevent Glide from decoding hardware bitmaps if Glide is * unable to detect that hardware bitmaps are unsafe. For example, you should set this to {@code * false} if you plan to draw it to a software {@link android.graphics.Canvas} or if you plan to * inspect the {@link Bitmap}s pixels with {@link Bitmap#getPixel(int, int)} or {@link * Bitmap#getPixels(int[], int, int, int, int, int, int)}. * *

    Callers can disable hardware {@link Bitmap}s for all loads using {@link * com.bumptech.glide.GlideBuilder#setDefaultRequestOptions(RequestOptions)}. * *

    This option is ignored unless we're on Android O+. */ public static final Option ALLOW_HARDWARE_CONFIG = Option.memory( "com.bumptech.glide.load.resource.bitmap.Downsampler.AllowHardwareDecode", false); private static final String WBMP_MIME_TYPE = "image/vnd.wap.wbmp"; private static final String ICO_MIME_TYPE = "image/x-ico"; private static final Set NO_DOWNSAMPLE_PRE_N_MIME_TYPES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(WBMP_MIME_TYPE, ICO_MIME_TYPE))); private static final DecodeCallbacks EMPTY_CALLBACKS = new DecodeCallbacks() { @Override public void onObtainBounds() { // Do nothing. } @Override public void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) { // Do nothing. } }; private static final Set TYPES_THAT_USE_POOL_PRE_KITKAT = Collections.unmodifiableSet( EnumSet.of( ImageHeaderParser.ImageType.JPEG, ImageHeaderParser.ImageType.PNG_A, ImageHeaderParser.ImageType.PNG)); private static final Queue OPTIONS_QUEUE = Util.createQueue(0); private final BitmapPool bitmapPool; private final DisplayMetrics displayMetrics; private final ArrayPool byteArrayPool; private final List parsers; private final HardwareConfigState hardwareConfigState = HardwareConfigState.getInstance(); public Downsampler( List parsers, DisplayMetrics displayMetrics, BitmapPool bitmapPool, ArrayPool byteArrayPool) { this.parsers = parsers; this.displayMetrics = Preconditions.checkNotNull(displayMetrics); this.bitmapPool = Preconditions.checkNotNull(bitmapPool); this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool); } public boolean handles(@SuppressWarnings("unused") InputStream is) { // We expect Downsampler to handle any available type Android supports. return true; } public boolean handles(@SuppressWarnings("unused") ByteBuffer byteBuffer) { // We expect downsampler to handle any available type Android supports. return true; } public boolean handles(@SuppressWarnings("unused") ParcelFileDescriptor source) { return ParcelFileDescriptorRewinder.isSupported(); } /** * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF * data present in the stream and that is downsampled according to the given dimensions and any * provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option. * * @see #decode(InputStream, int, int, Options, DecodeCallbacks) */ public Resource decode(InputStream is, int outWidth, int outHeight, Options options) throws IOException { return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS); } /** * Identical to {@link #decode(InputStream, int, int, Options)}, except that it accepts a {@link * ByteBuffer} in place of an {@link InputStream}. */ public Resource decode( ByteBuffer buffer, int requestedWidth, int requestedHeight, Options options) throws IOException { return decode( new ImageReader.ByteBufferReader(buffer, parsers, byteArrayPool), requestedWidth, requestedHeight, options, EMPTY_CALLBACKS); } /** * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF * data present in the stream and that is downsampled according to the given dimensions and any * provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option. * *

    If a Bitmap is present in the {@link * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} whose dimensions exactly match those * of the image for the given InputStream is available, the operation is much less expensive in * terms of memory. * * @param is An {@link InputStream} to the data for the image. * @param requestedWidth The width the final image should be close to. * @param requestedHeight The height the final image should be close to. * @param options A set of options that may contain one or more supported options that influence * how a Bitmap will be decoded from the given stream. * @param callbacks A set of callbacks allowing callers to optionally respond to various * significant events during the decode process. * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is * not null. */ public Resource decode( InputStream is, int requestedWidth, int requestedHeight, Options options, DecodeCallbacks callbacks) throws IOException { return decode( new ImageReader.InputStreamImageReader(is, parsers, byteArrayPool), requestedWidth, requestedHeight, options, callbacks); } @VisibleForTesting void decode(byte[] bytes, int requestedWidth, int requestedHeight, Options options) throws IOException { decode( new ImageReader.ByteArrayReader(bytes, parsers, byteArrayPool), requestedWidth, requestedHeight, options, EMPTY_CALLBACKS); } @VisibleForTesting void decode(File file, int requestedWidth, int requestedHeight, Options options) throws IOException { decode( new ImageReader.FileReader(file, parsers, byteArrayPool), requestedWidth, requestedHeight, options, EMPTY_CALLBACKS); } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public Resource decode( ParcelFileDescriptor parcelFileDescriptor, int outWidth, int outHeight, Options options) throws IOException { return decode( new ImageReader.ParcelFileDescriptorImageReader( parcelFileDescriptor, parsers, byteArrayPool), outWidth, outHeight, options, EMPTY_CALLBACKS); } private Resource decode( ImageReader imageReader, int requestedWidth, int requestedHeight, Options options, DecodeCallbacks callbacks) throws IOException { byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class); BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions(); bitmapFactoryOptions.inTempStorage = bytesForOptions; DecodeFormat decodeFormat = options.get(DECODE_FORMAT); PreferredColorSpace preferredColorSpace = options.get(PREFERRED_COLOR_SPACE); DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION); boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS); boolean isHardwareConfigAllowed = options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG); try { Bitmap result = decodeFromWrappedStreams( imageReader, bitmapFactoryOptions, downsampleStrategy, decodeFormat, preferredColorSpace, isHardwareConfigAllowed, requestedWidth, requestedHeight, fixBitmapToRequestedDimensions, callbacks); return BitmapResource.obtain(result, bitmapPool); } finally { releaseOptions(bitmapFactoryOptions); byteArrayPool.put(bytesForOptions); } } private Bitmap decodeFromWrappedStreams( ImageReader imageReader, BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, DecodeFormat decodeFormat, PreferredColorSpace preferredColorSpace, boolean isHardwareConfigAllowed, int requestedWidth, int requestedHeight, boolean fixBitmapToRequestedDimensions, DecodeCallbacks callbacks) throws IOException { long startTime = LogTime.getLogTime(); int[] sourceDimensions = getDimensions(imageReader, options, callbacks, bitmapPool); int sourceWidth = sourceDimensions[0]; int sourceHeight = sourceDimensions[1]; String sourceMimeType = options.outMimeType; // If we failed to obtain the image dimensions, we may end up with an incorrectly sized Bitmap, // so we want to use a mutable Bitmap type. One way this can happen is if the image header is so // large (10mb+) that our attempt to use inJustDecodeBounds fails and we're forced to decode the // full size image. if (sourceWidth == -1 || sourceHeight == -1) { isHardwareConfigAllowed = false; } int orientation = imageReader.getImageOrientation(); int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation); boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation); int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? (isRotationRequired(degreesToRotate) ? sourceHeight : sourceWidth) : requestedWidth; int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? (isRotationRequired(degreesToRotate) ? sourceWidth : sourceHeight) : requestedHeight; ImageType imageType = imageReader.getImageType(); calculateScaling( imageType, imageReader, callbacks, bitmapPool, downsampleStrategy, degreesToRotate, sourceWidth, sourceHeight, targetWidth, targetHeight, options); calculateConfig( imageReader, decodeFormat, isHardwareConfigAllowed, isExifOrientationRequired, options, targetWidth, targetHeight); boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding. if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) { int expectedWidth; int expectedHeight; if (sourceWidth >= 0 && sourceHeight >= 0 && fixBitmapToRequestedDimensions && isKitKatOrGreater) { expectedWidth = targetWidth; expectedHeight = targetHeight; } else { float densityMultiplier = isScaling(options) ? (float) options.inTargetDensity / options.inDensity : 1f; int sampleSize = options.inSampleSize; int downsampledWidth = (int) Math.ceil(sourceWidth / (float) sampleSize); int downsampledHeight = (int) Math.ceil(sourceHeight / (float) sampleSize); expectedWidth = Math.round(downsampledWidth * densityMultiplier); expectedHeight = Math.round(downsampledHeight * densityMultiplier); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Calculated target [" + expectedWidth + "x" + expectedHeight + "] for source" + " [" + sourceWidth + "x" + sourceHeight + "]" + ", sampleSize: " + sampleSize + ", targetDensity: " + options.inTargetDensity + ", density: " + options.inDensity + ", density multiplier: " + densityMultiplier); } } // If this isn't an image, or BitmapFactory was unable to parse the size, width and height // will be -1 here. if (expectedWidth > 0 && expectedHeight > 0) { setInBitmap(options, bitmapPool, expectedWidth, expectedHeight); } } if (preferredColorSpace != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { boolean isP3Eligible = preferredColorSpace == PreferredColorSpace.DISPLAY_P3 && options.outColorSpace != null && options.outColorSpace.isWideGamut(); options.inPreferredColorSpace = ColorSpace.get(isP3Eligible ? ColorSpace.Named.DISPLAY_P3 : ColorSpace.Named.SRGB); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); } } Bitmap downsampled = decodeStream(imageReader, options, callbacks, bitmapPool); callbacks.onDecodeComplete(bitmapPool, downsampled); if (Log.isLoggable(TAG, Log.VERBOSE)) { logDecode( sourceWidth, sourceHeight, sourceMimeType, options, downsampled, requestedWidth, requestedHeight, startTime); } Bitmap rotated = null; if (downsampled != null) { // If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to // the expected density dpi. downsampled.setDensity(displayMetrics.densityDpi); rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation); if (!downsampled.equals(rotated)) { bitmapPool.put(downsampled); } } return rotated; } private static void calculateScaling( ImageType imageType, ImageReader imageReader, DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool, DownsampleStrategy downsampleStrategy, int degreesToRotate, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight, BitmapFactory.Options options) throws IOException { // We can't downsample source content if we can't determine its dimensions. if (sourceWidth <= 0 || sourceHeight <= 0) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Unable to determine dimensions for: " + imageType + " with target [" + targetWidth + "x" + targetHeight + "]"); } return; } int orientedSourceWidth = sourceWidth; int orientedSourceHeight = sourceHeight; // If we're rotating the image +-90 degrees, we need to downsample accordingly so the image // width is decreased to near our target's height and the image height is decreased to near // our target width. //noinspection SuspiciousNameCombination if (isRotationRequired(degreesToRotate)) { orientedSourceWidth = sourceHeight; orientedSourceHeight = sourceWidth; } final float exactScaleFactor = downsampleStrategy.getScaleFactor( orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight); if (exactScaleFactor <= 0f) { throw new IllegalArgumentException( "Cannot scale with factor: " + exactScaleFactor + " from: " + downsampleStrategy + ", source: [" + sourceWidth + "x" + sourceHeight + "]" + ", target: [" + targetWidth + "x" + targetHeight + "]"); } SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding( orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight); if (rounding == null) { throw new IllegalArgumentException("Cannot round with null rounding"); } int outWidth = round(exactScaleFactor * orientedSourceWidth); int outHeight = round(exactScaleFactor * orientedSourceHeight); int widthScaleFactor = orientedSourceWidth / outWidth; int heightScaleFactor = orientedSourceHeight / outHeight; // TODO: This isn't really right for both CenterOutside and CenterInside. Consider allowing // DownsampleStrategy to pick, or trying to do something more sophisticated like picking the // scale factor that leads to an exact match. int scaleFactor = rounding == SampleSizeRounding.MEMORY ? Math.max(widthScaleFactor, heightScaleFactor) : Math.min(widthScaleFactor, heightScaleFactor); int powerOfTwoSampleSize; // BitmapFactory does not support downsampling wbmp files on platforms <= M. See b/27305903. if (Build.VERSION.SDK_INT <= 23 && NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) { powerOfTwoSampleSize = 1; } else { powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor)); if (rounding == SampleSizeRounding.MEMORY && powerOfTwoSampleSize < (1.f / exactScaleFactor)) { powerOfTwoSampleSize = powerOfTwoSampleSize << 1; } } // Here we mimic framework logic for determining how inSampleSize division is rounded on various // versions of Android. The logic here has been tested on emulators for Android versions 15-26. // PNG - Always uses floor // JPEG - Always uses ceiling // Webp - Prior to N, always uses floor. At and after N, always uses round. options.inSampleSize = powerOfTwoSampleSize; int powerOfTwoWidth; int powerOfTwoHeight; if (imageType == ImageType.JPEG) { // libjpegturbo can downsample up to a sample size of 8. libjpegturbo uses ceiling to round. // After libjpegturbo's native rounding, skia does a secondary scale using floor // (integer division). Here we replicate that logic. int nativeScaling = Math.min(powerOfTwoSampleSize, 8); powerOfTwoWidth = (int) Math.ceil(orientedSourceWidth / (float) nativeScaling); powerOfTwoHeight = (int) Math.ceil(orientedSourceHeight / (float) nativeScaling); int secondaryScaling = powerOfTwoSampleSize / 8; if (secondaryScaling > 0) { powerOfTwoWidth = powerOfTwoWidth / secondaryScaling; powerOfTwoHeight = powerOfTwoHeight / secondaryScaling; } } else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) { powerOfTwoWidth = (int) Math.floor(orientedSourceWidth / (float) powerOfTwoSampleSize); powerOfTwoHeight = (int) Math.floor(orientedSourceHeight / (float) powerOfTwoSampleSize); } else if (imageType.isWebp()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { powerOfTwoWidth = Math.round(orientedSourceWidth / (float) powerOfTwoSampleSize); powerOfTwoHeight = Math.round(orientedSourceHeight / (float) powerOfTwoSampleSize); } else { powerOfTwoWidth = (int) Math.floor(orientedSourceWidth / (float) powerOfTwoSampleSize); powerOfTwoHeight = (int) Math.floor(orientedSourceHeight / (float) powerOfTwoSampleSize); } } else if (orientedSourceWidth % powerOfTwoSampleSize != 0 || orientedSourceHeight % powerOfTwoSampleSize != 0) { // If we're not confident the image is in one of our types, fall back to checking the // dimensions again. inJustDecodeBounds decodes do obey inSampleSize. int[] dimensions = getDimensions(imageReader, options, decodeCallbacks, bitmapPool); // Power of two downsampling in BitmapFactory uses a variety of random factors to determine // rounding that we can't reliably replicate for all image formats. Use ceiling here to make // sure that we at least provide a Bitmap that's large enough to fit the content we're going // to load. powerOfTwoWidth = dimensions[0]; powerOfTwoHeight = dimensions[1]; } else { powerOfTwoWidth = orientedSourceWidth / powerOfTwoSampleSize; powerOfTwoHeight = orientedSourceHeight / powerOfTwoSampleSize; } double adjustedScaleFactor = downsampleStrategy.getScaleFactor( powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight); // Density scaling is only supported if inBitmap is null prior to KitKat. Avoid setting // densities here so we calculate the final Bitmap size correctly. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor); options.inDensity = getDensityMultiplier(adjustedScaleFactor); } if (isScaling(options)) { options.inScaled = true; } else { options.inDensity = options.inTargetDensity = 0; } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Calculate scaling" + ", source: [" + sourceWidth + "x" + sourceHeight + "]" + ", degreesToRotate: " + degreesToRotate + ", target: [" + targetWidth + "x" + targetHeight + "]" + ", power of two scaled: [" + powerOfTwoWidth + "x" + powerOfTwoHeight + "]" + ", exact scale factor: " + exactScaleFactor + ", power of 2 sample size: " + powerOfTwoSampleSize + ", adjusted scale factor: " + adjustedScaleFactor + ", target density: " + options.inTargetDensity + ", density: " + options.inDensity); } } /** * BitmapFactory calculates the density scale factor as a float. This introduces some non-trivial * error. This method attempts to account for that error by adjusting the inTargetDensity so that * the final scale factor is as close to our target as possible. */ private static int adjustTargetDensityForError(double adjustedScaleFactor) { int densityMultiplier = getDensityMultiplier(adjustedScaleFactor); int targetDensity = round(densityMultiplier * adjustedScaleFactor); float scaleFactorWithError = targetDensity / (float) densityMultiplier; double difference = adjustedScaleFactor / scaleFactorWithError; return round(difference * targetDensity); } private static int getDensityMultiplier(double adjustedScaleFactor) { return (int) Math.round( Integer.MAX_VALUE * (adjustedScaleFactor <= 1D ? adjustedScaleFactor : 1 / adjustedScaleFactor)); } // This is weird, but it matches the logic in a bunch of Android views/framework classes for // rounding. private static int round(double value) { return (int) (value + 0.5d); } private boolean shouldUsePool(ImageType imageType) { // On KitKat+, any bitmap (of a given config) can be used to decode any other bitmap // (with the same config). if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return true; } // We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat. // See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(imageType); } @SuppressWarnings("deprecation") private void calculateConfig( ImageReader imageReader, DecodeFormat format, boolean isHardwareConfigAllowed, boolean isExifOrientationRequired, BitmapFactory.Options optionsWithScaling, int targetWidth, int targetHeight) { if (hardwareConfigState.setHardwareConfigIfAllowed( targetWidth, targetHeight, optionsWithScaling, isHardwareConfigAllowed, isExifOrientationRequired)) { return; } // Changing configs can cause skewing on 4.1, see issue #128. if (format == DecodeFormat.PREFER_ARGB_8888 || Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888; return; } boolean hasAlpha = false; try { hasAlpha = imageReader.getImageType().hasAlpha(); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Cannot determine whether the image has alpha or not from header" + ", format " + format, e); } } optionsWithScaling.inPreferredConfig = hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; if (optionsWithScaling.inPreferredConfig == Config.RGB_565) { optionsWithScaling.inDither = true; } } /** * A method for getting the dimensions of an image from the given InputStream. * * @param imageReader The {@link ImageReader} representing the image. * @param options The options to pass to {@link BitmapFactory#decodeStream(java.io.InputStream, * android.graphics.Rect, android.graphics.BitmapFactory.Options)}. * @return an array containing the dimensions of the image in the form {width, height}. */ private static int[] getDimensions( ImageReader imageReader, BitmapFactory.Options options, DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool) throws IOException { options.inJustDecodeBounds = true; decodeStream(imageReader, options, decodeCallbacks, bitmapPool); options.inJustDecodeBounds = false; return new int[] {options.outWidth, options.outHeight}; } private static Bitmap decodeStream( ImageReader imageReader, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException { if (!options.inJustDecodeBounds) { // Once we've read the image header, we no longer need to allow the buffer to expand in // size. To avoid unnecessary allocations reading image data, we fix the mark limit so that it // is no larger than our current buffer size here. We need to do so immediately before // decoding the full image to avoid having our mark limit overridden by other calls to // mark and reset. See issue #225. callbacks.onObtainBounds(); imageReader.stopGrowingBuffers(); } // BitmapFactory.Options out* variables are reset by most calls to decodeStream, successful or // otherwise, so capture here in case we log below. int sourceWidth = options.outWidth; int sourceHeight = options.outHeight; String outMimeType = options.outMimeType; final Bitmap result; TransformationUtils.getBitmapDrawableLock().lock(); try { result = imageReader.decodeBitmap(options); } catch (IllegalArgumentException e) { IOException bitmapAssertionException = newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Failed to decode with inBitmap, trying again without Bitmap re-use", bitmapAssertionException); } if (options.inBitmap != null) { try { bitmapPool.put(options.inBitmap); options.inBitmap = null; return decodeStream(imageReader, options, callbacks, bitmapPool); } catch (IOException resetException) { throw bitmapAssertionException; } } throw bitmapAssertionException; } finally { TransformationUtils.getBitmapDrawableLock().unlock(); } return result; } private static boolean isScaling(BitmapFactory.Options options) { return options.inTargetDensity > 0 && options.inDensity > 0 && options.inTargetDensity != options.inDensity; } private static void logDecode( int sourceWidth, int sourceHeight, String outMimeType, BitmapFactory.Options options, Bitmap result, int requestedWidth, int requestedHeight, long startTime) { Log.v( TAG, "Decoded " + getBitmapString(result) + " from [" + sourceWidth + "x" + sourceHeight + "] " + outMimeType + " with inBitmap " + getInBitmapString(options) + " for [" + requestedWidth + "x" + requestedHeight + "]" + ", sample size: " + options.inSampleSize + ", density: " + options.inDensity + ", target density: " + options.inTargetDensity + ", thread: " + Thread.currentThread().getName() + ", duration: " + LogTime.getElapsedMillis(startTime)); } private static String getInBitmapString(BitmapFactory.Options options) { return getBitmapString(options.inBitmap); } @Nullable @TargetApi(Build.VERSION_CODES.KITKAT) private static String getBitmapString(Bitmap bitmap) { if (bitmap == null) { return null; } String sizeString = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? " (" + bitmap.getAllocationByteCount() + ")" : ""; return "[" + bitmap.getWidth() + "x" + bitmap.getHeight() + "] " + bitmap.getConfig() + sizeString; } // BitmapFactory throws an IllegalArgumentException if any error occurs attempting to decode a // file when inBitmap is non-null, including those caused by partial or corrupt data. We still log // the error because the IllegalArgumentException is supposed to catch errors reusing Bitmaps, so // want some useful log output. In most cases this can be safely treated as a normal IOException. private static IOException newIoExceptionForInBitmapAssertion( IllegalArgumentException e, int outWidth, int outHeight, String outMimeType, BitmapFactory.Options options) { return new IOException( "Exception decoding bitmap" + ", outWidth: " + outWidth + ", outHeight: " + outHeight + ", outMimeType: " + outMimeType + ", inBitmap: " + getInBitmapString(options), e); } @SuppressWarnings("PMD.CollapsibleIfStatements") @TargetApi(Build.VERSION_CODES.O) private static void setInBitmap( BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) { @Nullable Bitmap.Config expectedConfig = null; // Avoid short circuiting, it appears to break on some devices. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (options.inPreferredConfig == Config.HARDWARE) { return; } // On API 26 outConfig may be null for some images even if the image is valid, can be decoded // and outWidth/outHeight/outColorSpace are populated (see b/71513049). expectedConfig = options.outConfig; } if (expectedConfig == null) { // We're going to guess that BitmapFactory will return us the config we're requesting. This // isn't always the case, even though our guesses tend to be conservative and prefer configs // of larger sizes so that the Bitmap will fit our image anyway. If we're wrong here and the // config we choose is too small, our initial decode will fail, but we will retry with no // inBitmap which will succeed so if we're wrong here, we're less efficient but still correct. expectedConfig = options.inPreferredConfig; } // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe. options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig); } private static synchronized BitmapFactory.Options getDefaultOptions() { BitmapFactory.Options decodeBitmapOptions; synchronized (OPTIONS_QUEUE) { decodeBitmapOptions = OPTIONS_QUEUE.poll(); } if (decodeBitmapOptions == null) { decodeBitmapOptions = new BitmapFactory.Options(); resetOptions(decodeBitmapOptions); } return decodeBitmapOptions; } private static void releaseOptions(BitmapFactory.Options decodeBitmapOptions) { resetOptions(decodeBitmapOptions); synchronized (OPTIONS_QUEUE) { OPTIONS_QUEUE.offer(decodeBitmapOptions); } } @SuppressWarnings("deprecation") private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) { decodeBitmapOptions.inTempStorage = null; decodeBitmapOptions.inDither = false; decodeBitmapOptions.inScaled = false; decodeBitmapOptions.inSampleSize = 1; decodeBitmapOptions.inPreferredConfig = null; decodeBitmapOptions.inJustDecodeBounds = false; decodeBitmapOptions.inDensity = 0; decodeBitmapOptions.inTargetDensity = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { decodeBitmapOptions.inPreferredColorSpace = null; decodeBitmapOptions.outColorSpace = null; decodeBitmapOptions.outConfig = null; } decodeBitmapOptions.outWidth = 0; decodeBitmapOptions.outHeight = 0; decodeBitmapOptions.outMimeType = null; decodeBitmapOptions.inBitmap = null; decodeBitmapOptions.inMutable = true; } /** Callbacks for key points during decodes. */ public interface DecodeCallbacks { void onObtainBounds(); void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) throws IOException; } private static boolean isRotationRequired(int degreesToRotate) { return degreesToRotate == 90 || degreesToRotate == 270; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/DrawableToBitmapConverter.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Animatable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.Log; import androidx.annotation.Nullable; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.request.target.Target; import java.util.concurrent.locks.Lock; final class DrawableToBitmapConverter { private static final String TAG = "DrawableToBitmap"; private static final BitmapPool NO_RECYCLE_BITMAP_POOL = new BitmapPoolAdapter() { @Override public void put(Bitmap bitmap) { // Avoid calling super to avoid recycling the given Bitmap. } }; private DrawableToBitmapConverter() { // Utility class. } @Nullable static Resource convert(BitmapPool bitmapPool, Drawable drawable, int width, int height) { // Handle DrawableContainer or StateListDrawables that may contain one or more BitmapDrawables. drawable = drawable.getCurrent(); Bitmap result = null; boolean isRecycleable = false; if (drawable instanceof BitmapDrawable) { result = ((BitmapDrawable) drawable).getBitmap(); } else if (!(drawable instanceof Animatable)) { result = drawToBitmap(bitmapPool, drawable, width, height); // We created and drew to the Bitmap, so it's safe for us to recycle or re-use. isRecycleable = true; } BitmapPool toUse = isRecycleable ? bitmapPool : NO_RECYCLE_BITMAP_POOL; return BitmapResource.obtain(result, toUse); } @Nullable private static Bitmap drawToBitmap( BitmapPool bitmapPool, Drawable drawable, int width, int height) { if (width == Target.SIZE_ORIGINAL && drawable.getIntrinsicWidth() <= 0) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w( TAG, "Unable to draw " + drawable + " to Bitmap with Target.SIZE_ORIGINAL because the" + " Drawable has no intrinsic width"); } return null; } if (height == Target.SIZE_ORIGINAL && drawable.getIntrinsicHeight() <= 0) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w( TAG, "Unable to draw " + drawable + " to Bitmap with Target.SIZE_ORIGINAL because the" + " Drawable has no intrinsic height"); } return null; } int targetWidth = drawable.getIntrinsicWidth() > 0 ? drawable.getIntrinsicWidth() : width; int targetHeight = drawable.getIntrinsicHeight() > 0 ? drawable.getIntrinsicHeight() : height; Lock lock = TransformationUtils.getBitmapDrawableLock(); lock.lock(); Bitmap result = bitmapPool.get(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); try { Canvas canvas = new Canvas(result); drawable.setBounds(0, 0, targetWidth, targetHeight); drawable.draw(canvas); canvas.setBitmap(null); } finally { lock.unlock(); } return result; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/DrawableTransformation.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import java.security.MessageDigest; /** * Applies a {@link Bitmap} {@link Transformation} to {@link Drawable}s by first attempting to * convert the {@link Drawable} to a {@link Bitmap} and then running the {@link Transformation} on * the converted {@link Bitmap}. * *

    This class is relatively efficient for {@link BitmapDrawable} where the {@link Bitmap} is * readily accessible. For non-{@link Bitmap} based {@link Drawable}s, this class must first try to * draw the {@link Drawable} to a {@link Bitmap} using {@link android.graphics.Canvas}, which is * less efficient. {@link Drawable}s that implement {@link android.graphics.drawable.Animatable} * will fail with an exception. {@link Drawable}s that return {@code <= 0} for {@link * Drawable#getIntrinsicHeight()} and/or {@link Drawable#getIntrinsicWidth()} will fail with an * exception if the requested size is {@link * com.bumptech.glide.request.target.Target#SIZE_ORIGINAL}. {@link Drawable}s without intrinsic * dimensions are drawn using the dimensions provided in {@link Transformation#transform(Context, * Resource, int, int)}. As a result, they may be transformed incorrectly or in unexpected ways. */ public class DrawableTransformation implements Transformation { private final Transformation wrapped; private final boolean isRequired; public DrawableTransformation(Transformation wrapped, boolean isRequired) { this.wrapped = wrapped; this.isRequired = isRequired; } @SuppressWarnings("unchecked") public Transformation asBitmapDrawable() { return (Transformation) (Transformation) this; } @NonNull @Override public Resource transform( @NonNull Context context, @NonNull Resource resource, int outWidth, int outHeight) { BitmapPool bitmapPool = Glide.get(context).getBitmapPool(); Drawable drawable = resource.get(); Resource bitmapResourceToTransform = DrawableToBitmapConverter.convert(bitmapPool, drawable, outWidth, outHeight); if (bitmapResourceToTransform == null) { if (isRequired) { throw new IllegalArgumentException("Unable to convert " + drawable + " to a Bitmap"); } else { return resource; } } Resource transformedBitmapResource = wrapped.transform(context, bitmapResourceToTransform, outWidth, outHeight); if (transformedBitmapResource.equals(bitmapResourceToTransform)) { transformedBitmapResource.recycle(); return resource; } else { return newDrawableResource(context, transformedBitmapResource); } } // It's clearer to cast the result in a separate line from obtaining it. @SuppressWarnings({"unchecked", "PMD.UnnecessaryLocalBeforeReturn"}) private Resource newDrawableResource(Context context, Resource transformed) { Resource result = LazyBitmapDrawableResource.obtain(context.getResources(), transformed); return (Resource) result; } @Override public boolean equals(Object o) { if (o instanceof DrawableTransformation) { DrawableTransformation other = (DrawableTransformation) o; return wrapped.equals(other.wrapped); } return false; } @Override public int hashCode() { return wrapped.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { wrapped.updateDiskCacheKey(messageDigest); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/ExifInterfaceImageHeaderParser.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.exifinterface.media.ExifInterface; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.util.ByteBufferUtil; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; /** * Uses {@link ExifInterface} to parse orientation data. * *

    ExifInterface supports the HEIF format on OMR1+. Glide's {@link DefaultImageHeaderParser} * doesn't currently support HEIF. In the future we should reconcile these two classes, but for now * this is a simple way to ensure that HEIF files are oriented correctly on platforms where they're * supported. */ @RequiresApi(Build.VERSION_CODES.O_MR1) public final class ExifInterfaceImageHeaderParser implements ImageHeaderParser { @NonNull @Override public ImageType getType(@NonNull InputStream is) { return ImageType.UNKNOWN; } @NonNull @Override public ImageType getType(@NonNull ByteBuffer byteBuffer) { return ImageType.UNKNOWN; } @Override public int getOrientation(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { ExifInterface exifInterface = new ExifInterface(is); int result = exifInterface.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); if (result == ExifInterface.ORIENTATION_UNDEFINED) { return ImageHeaderParser.UNKNOWN_ORIENTATION; } return result; } @Override public int getOrientation(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException { return getOrientation(ByteBufferUtil.toStream(byteBuffer), byteArrayPool); } @Override public boolean hasJpegMpf(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { return false; } @Override public boolean hasJpegMpf(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException { return false; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/FitCenter.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import java.security.MessageDigest; /** * Scales the image uniformly (maintaining the image's aspect ratio) so that one of the dimensions * of the image will be equal to the given dimension and the other will be less than the given * dimension. */ public class FitCenter extends BitmapTransformation { private static final String ID = "com.bumptech.glide.load.resource.bitmap.FitCenter"; private static final byte[] ID_BYTES = ID.getBytes(CHARSET); @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return TransformationUtils.fitCenter(pool, toTransform, outWidth, outHeight); } @Override public boolean equals(Object o) { return o instanceof FitCenter; } @Override public int hashCode() { return ID.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(ID_BYTES); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/GlideBitmapFactory.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.graphics.Canvas; import android.graphics.ColorMatrixColorFilter; import android.graphics.Gainmap; import android.graphics.Paint; import android.graphics.Rect; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.bumptech.glide.util.GlideSuppliers; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import com.bumptech.glide.util.Preconditions; import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; /** * Wrapper around {@link BitmapFactory} to work around known issues with {@link BitmapFactory} * across Android SDK levels. * *

    In particular, this class works around these known issues: * *

      *
    • Ultra HDR image single-channel gainmaps not being decoded on Android U when hardware * bitmaps are enabled. This issue is further described in * https://github.com/bumptech/glide/issues/5362. *
    * *

    New usages of {@link BitmapFactory} APIs within Glide should be added here rather than called * directly. */ final class GlideBitmapFactory { private static final String TAG = "GlideBitmapFactory"; private GlideBitmapFactory() {} /** Wrapper for {@link BitmapFactory#decodeStream}. */ @Nullable public static Bitmap decodeStream( InputStream inputStream, BitmapFactory.Options options, ImageReader reader) { if (VERSION.SDK_INT == VERSION_CODES.UPSIDE_DOWN_CAKE && GainmapDecoderWorkaroundStateCalculator.needsGainmapDecodeWorkaround(options) && isLikelyToContainGainmap(reader)) { return safeAndExpensiveDecodeHardwareBitmapWithGainmap(inputStream, options); } return BitmapFactory.decodeStream(inputStream, /* outPadding= */ null, options); } /** Wrapper for {@link BitmapFactory#decodeByteArray}. */ @Nullable public static Bitmap decodeByteArray( byte[] bytes, BitmapFactory.Options options, ImageReader reader) { if (VERSION.SDK_INT == VERSION_CODES.UPSIDE_DOWN_CAKE && GainmapDecoderWorkaroundStateCalculator.needsGainmapDecodeWorkaround(options) && isLikelyToContainGainmap(reader)) { return safeAndExpensiveDecodeHardwareBitmapWithGainmap(bytes, options); } return BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options); } /** Wrapper for {@link BitmapFactory#decodeFileDescriptor}. */ @Nullable public static Bitmap decodeFileDescriptor( FileDescriptor fileDescriptor, BitmapFactory.Options options, ImageReader reader) { if (VERSION.SDK_INT == VERSION_CODES.UPSIDE_DOWN_CAKE && GainmapDecoderWorkaroundStateCalculator.needsGainmapDecodeWorkaround(options) && isLikelyToContainGainmap(reader)) { return safeAndExpensiveDecodeHardwareBitmapWithGainmap(fileDescriptor, options); } return BitmapFactory.decodeFileDescriptor(fileDescriptor, /* outPadding= */ null, options); } /** * Returns whether the image referenced by the {@link ImageReader} is likely to have a gainmap. * *

    On Android devices, a JPEG with multi-picture format (MPF) metadata is very likely to * contain a gainmap, either it being an Ultra HDR JPEG or a ISO 21496-1 JPEG. */ private static boolean isLikelyToContainGainmap(ImageReader imageReader) { try { boolean hasMpf = imageReader.hasJpegMpf(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "isLikelyToContainGainmap=" + hasMpf); } return hasMpf; } catch (IOException e) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "isLikelyToContainGainmap failed", e); } } return false; } /** * Returns a decoded bitmap for the input stream, ensuring that any associated gainmap is decoded * without being silently dropped on Android U. * *

    If the input stream does not reference an image with a gainmap, then this method simply * returns a hardware bitmap. * *

    This method safely wraps BitmapFactory#decodeStream(InputStream, Rect, Options)} on Android * U. * *

    This method performs an expensive workaround, using software bitmap decoding. It is * recommended to only use this check on images that have a reasonable chance of containing * gainmaps (e.g., they already contain JPEG multi-picture format metadata). * * @param inputStream for the bitmap to be decoded. * @param options to be applied in the {@link BitmapFactory#decodeStream} call. */ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) @Nullable private static Bitmap safeAndExpensiveDecodeHardwareBitmapWithGainmap( InputStream inputStream, Options options) { Preconditions.checkArgument(options.inPreferredConfig == Config.HARDWARE); Bitmap softwareBitmap = null; options.inPreferredConfig = Config.ARGB_8888; try { softwareBitmap = BitmapFactory.decodeStream(inputStream, /* outPadding= */ null, options); if (softwareBitmap == null) { return null; } return safeDecodeBitmapWithGainmap(softwareBitmap); } finally { if (softwareBitmap != null) { softwareBitmap.recycle(); } options.inPreferredConfig = Config.HARDWARE; } } /** * Returns a decoded bitmap for the input byte array, ensuring that any associated gainmap is * decoded without being silently dropped on Android U. * *

    If the input bytes do not reference an image with a gainmap, then this method simply returns * a hardware bitmap. * *

    This method safely wraps BitmapFactory#decodeByteArray(byte[], int, int)} on Android U. * * @param bytes for the bitmap to be decoded. * @param options to be applied in the {@link BitmapFactory#decodeByteArray} call. This must be * set to {@link Config#HARDWARE}. * @throws IllegalArgumentException if {@link Options#inPreferredConfig} is set to any state other * than {@link Config#HARDWARE}. */ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) @Nullable private static Bitmap safeAndExpensiveDecodeHardwareBitmapWithGainmap( byte[] bytes, Options options) { Preconditions.checkArgument(options.inPreferredConfig == Config.HARDWARE); options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap softwareBitmap = null; try { softwareBitmap = BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options); if (softwareBitmap == null) { return null; } return GlideBitmapFactory.safeDecodeBitmapWithGainmap(softwareBitmap); } finally { if (softwareBitmap != null) { softwareBitmap.recycle(); } options.inPreferredConfig = Config.HARDWARE; } } /** * Returns a decoded bitmap for the input file descriptor, ensuring that any associated gainmap is * decoded without being silently dropped on Android U. * *

    If the input file descriptor does not reference an image with a gainmap, then this method * simply returns a hardware bitmap. * *

    This method safely wraps {@link BitmapFactory#decodeFileDescriptor(FileDescriptor, Rect, * Options)} on Android U. * * @param fileDescriptor from which the bitmap will be decoded. * @param options to be applied in the {@link BitmapFactory#decodeFileDescriptor} call. This must * be set to {@link Config#HARDWARE}. * @throws IllegalArgumentException if {@link Options#inPreferredConfig} is set to any state other * than {@link Config#HARDWARE}. */ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) @Nullable private static Bitmap safeAndExpensiveDecodeHardwareBitmapWithGainmap( FileDescriptor fileDescriptor, Options options) { Preconditions.checkArgument(options.inPreferredConfig == Config.HARDWARE); Bitmap softwareBitmap = null; options.inPreferredConfig = Bitmap.Config.ARGB_8888; try { softwareBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, /* outPadding= */ null, options); if (softwareBitmap == null) { return null; } return GlideBitmapFactory.safeDecodeBitmapWithGainmap(softwareBitmap); } finally { if (softwareBitmap != null) { softwareBitmap.recycle(); } options.inPreferredConfig = Config.HARDWARE; } } /** * Returns a decoded bitmap for the input software bitmap, ensuring that any associated gainmap is * decoded without errors on Android U if it is a valid gainmap. * * @param softwareBitmap The bitmap to be decoded. Must not be a hardware bitmap. The caller of * this method is responsible for recycling this bitmap. * @throws IllegalArgumentException if {@link Options#inPreferredConfig} is set to any state other * than {@link Config#HARDWARE}. */ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) @Nullable private static Bitmap safeDecodeBitmapWithGainmap(Bitmap softwareBitmap) { Gainmap gainmap = softwareBitmap.getGainmap(); if (gainmap != null) { Bitmap gainmapContents = gainmap.getGainmapContents(); if (gainmapContents.getConfig() == Config.ALPHA_8) { softwareBitmap.setGainmap( GainmapCopier.convertSingleChannelGainmapToTripleChannelGainmap(gainmap)); } } return softwareBitmap.copy(Config.HARDWARE, /* isMutable= */ false); } /** Utils to copy gainmaps. */ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) private static final class GainmapCopier { /** Transforms a bitmap so that the output alpha is opaque. */ private static final ColorMatrixColorFilter OPAQUE_FILTER = new ColorMatrixColorFilter( new float[] { 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 255f }); private GainmapCopier() {} /** * Converts single channel gainmap to triple channel, where a single channel gainmap is defined * as a gainmap with a bitmap config of {@link Config#ALPHA_8}. * *

    If the input gainmap is not single channel or the copy operation fails, then this method * will just return the original gainmap. */ public static Gainmap convertSingleChannelGainmapToTripleChannelGainmap(Gainmap gainmap) { Bitmap gainmapContents = gainmap.getGainmapContents(); if (gainmapContents.getConfig() != Config.ALPHA_8) { return gainmap; } Bitmap newContents = copyAlpha8ToOpaqueArgb888(gainmapContents); Gainmap newGainmap = new Gainmap(newContents); float[] tempFloatArray = gainmap.getRatioMin(); newGainmap.setRatioMin(tempFloatArray[0], tempFloatArray[1], tempFloatArray[2]); tempFloatArray = gainmap.getRatioMax(); newGainmap.setRatioMax(tempFloatArray[0], tempFloatArray[1], tempFloatArray[2]); tempFloatArray = gainmap.getGamma(); newGainmap.setGamma(tempFloatArray[0], tempFloatArray[1], tempFloatArray[2]); tempFloatArray = gainmap.getEpsilonSdr(); newGainmap.setEpsilonSdr(tempFloatArray[0], tempFloatArray[1], tempFloatArray[2]); tempFloatArray = gainmap.getEpsilonHdr(); newGainmap.setEpsilonHdr(tempFloatArray[0], tempFloatArray[1], tempFloatArray[2]); newGainmap.setDisplayRatioForFullHdr(gainmap.getDisplayRatioForFullHdr()); newGainmap.setMinDisplayRatioForHdrTransition(gainmap.getMinDisplayRatioForHdrTransition()); return newGainmap; } /** * Converts an {@link Config#ALPHA_8} bitmap to a {@link Config#ARGB_8888} bitmap with the alpha * channel set to unity so that the output bitmap is opaque. * * @throws IllegalArgumentException if called with a bitmap with a config that is not {@link * Config#ALPHA_8} */ private static Bitmap copyAlpha8ToOpaqueArgb888(Bitmap bitmap) { Preconditions.checkArgument(bitmap.getConfig() == Config.ALPHA_8); // We have to use a canvas operation with an opaque alpha filter to draw the gainmap. We can't // use bitmap.copy(Config.ARGB_8888, /* isMutable= */ false) because copying from A8 to RBGA // will result in zero-valued RGB values. Bitmap newContents = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888); Canvas canvas = new Canvas(newContents); Paint paint = new Paint(); paint.setColorFilter(OPAQUE_FILTER); canvas.drawBitmap(bitmap, /* left= */ 0f, /* top= */ 0f, paint); canvas.setBitmap(null); return newContents; } } /** * Determines if a gainmap decoding workaround is required to mitigate an Android U bug with * decoding bitmaps with gainmaps. When the following conditions are present, Android U will not * be able to decode a gainmap, with hardware bitmap operation failing for the gainmap: * *

      *
    • The HWUI is configured to use skiagl. *
    • The gainmap is single channel. *
    • The bitmap owning the bitmap is a hardware bitmap. *
    * *

    Callers should use this class to determine whether to apply a workaround, e.g., modifying * the gainmap to be triple channel and software decode it. */ public static final class GainmapDecoderWorkaroundStateCalculator { private static final String TAG = "GainmapWorkaroundCalc"; /** Meomizes result of test to see if the device is susceptible to the gainmap decoding bug. */ private static final GlideSupplier REQUIRES_GAIN_MAP_FIX = GlideSuppliers.memorize(() -> calculateNeedsGainmapDecodeWorkaround()); private GainmapDecoderWorkaroundStateCalculator() {} /** * Returns true if a gainmap decoding workaround is required to mitigate an Android U bug. This * method tests for the presence of the bug, which only affects hardware bitmaps, and caches the * result in memory. * *

    This method is thread-safe. * * @param options which will be used to decode the gainmap. */ private static boolean needsGainmapDecodeWorkaround(Options options) { if (VERSION.SDK_INT != VERSION_CODES.UPSIDE_DOWN_CAKE) { return false; } if (options.inPreferredConfig != Config.HARDWARE) { return false; } return REQUIRES_GAIN_MAP_FIX.get(); } private static boolean calculateNeedsGainmapDecodeWorkaround() { if (VERSION.SDK_INT != VERSION_CODES.UPSIDE_DOWN_CAKE) { return false; } // Create a 1x1 single channel, A8 bitmap and attempt to copy to a hardware bitmap. If the // copy operation fails, then the device requires a workaround to decode hardware // gainmaps. Bitmap a8Source = Bitmap.createBitmap(/* width= */ 1, /* height= */ 1, Config.ALPHA_8); Bitmap a8HardwareBitmap = a8Source.copy(Config.HARDWARE, /* isMutable= */ false); a8Source.recycle(); boolean needsGainmapDecodeWorkaround = a8HardwareBitmap == null; if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "calculateNeedsGainmapDecodeWorkaround=" + needsGainmapDecodeWorkaround); } if (a8HardwareBitmap != null) { a8HardwareBitmap.recycle(); } return needsGainmapDecodeWorkaround; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/GranularRoundedCorners.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Util; import java.nio.ByteBuffer; import java.security.MessageDigest; /** A {@link BitmapTransformation} which has a different radius for each corner of a bitmap. */ public final class GranularRoundedCorners extends BitmapTransformation { private static final String ID = "com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners"; private static final byte[] ID_BYTES = ID.getBytes(CHARSET); private final float topLeft; private final float topRight; private final float bottomRight; private final float bottomLeft; /** Provide the radii to round the corners of the bitmap. */ public GranularRoundedCorners( float topLeft, float topRight, float bottomRight, float bottomLeft) { this.topLeft = topLeft; this.topRight = topRight; this.bottomRight = bottomRight; this.bottomLeft = bottomLeft; } @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return TransformationUtils.roundedCorners( pool, toTransform, topLeft, topRight, bottomRight, bottomLeft); } @Override public boolean equals(Object o) { if (o instanceof GranularRoundedCorners) { GranularRoundedCorners other = (GranularRoundedCorners) o; return topLeft == other.topLeft && topRight == other.topRight && bottomRight == other.bottomRight && bottomLeft == other.bottomLeft; } return false; } @Override public int hashCode() { int hashCode = Util.hashCode(ID.hashCode(), Util.hashCode(topLeft)); hashCode = Util.hashCode(topRight, hashCode); hashCode = Util.hashCode(bottomRight, hashCode); return Util.hashCode(bottomLeft, hashCode); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(ID_BYTES); byte[] radiusData = ByteBuffer.allocate(16) .putFloat(topLeft) .putFloat(topRight) .putFloat(bottomRight) .putFloat(bottomLeft) .array(); messageDigest.update(radiusData); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/HardwareConfigState.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Build.VERSION_CODES; import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.GuardedBy; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.util.Util; import java.io.File; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; /** * State and constants for interacting with {@link android.graphics.Bitmap.Config#HARDWARE} on * Android O+. */ public final class HardwareConfigState { private static final String TAG = "HardwareConfig"; /** * Force the state to wait until a call to allow hardware Bitmaps to be used when they'd otherwise * be eligible to work around a framework issue pre Q that can cause a native crash when * allocating a hardware Bitmap in this specific circumstance. See b/126573603#comment12 for * details. */ public static final boolean BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q; /** Support for the hardware bitmap config was added in Android O. */ @ChecksSdkIntAtLeast(api = VERSION_CODES.P) public static final boolean HARDWARE_BITMAPS_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; /** * Allows us to check to make sure we're not exceeding the FD limit for a process with hardware * {@link Bitmap}s. * *

    {@link Bitmap.Config#HARDWARE} {@link Bitmap}s require two FDs (depending on the driver). * Processes have an FD limit of 1024 (at least on O). With sufficiently small {@link Bitmap}s * and/or a sufficiently large {@link com.bumptech.glide.load.engine.cache.MemoryCache}, we can * end up with enough {@link Bitmap}s in memory that we blow through the FD limit, which causes * graphics errors, Binder errors, and a variety of crashes. * *

    Calling list.size() should be relatively efficient (hopefully < 1ms on average) because * /proc is an in-memory FS. */ private static final File FD_SIZE_LIST = new File("/proc/self/fd"); /** * Each FD check takes 1-2ms, so to avoid overhead, only check every N decodes. 50 is more or less * arbitrary. */ private static final int MINIMUM_DECODES_BETWEEN_FD_CHECKS = 50; // 20k. private static final int MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_P = 20000; /** * Some P devices seem to have a more O like FD count, so we'll manually reduce the number of FDs * we use for hardware bitmaps. See b/139097735. */ private static final int REDUCED_MAX_FDS_FOR_HARDWARE_CONFIGS_P = 500; /** * @deprecated This constant is unused and will be removed in a future version, avoid using it. */ @Deprecated public static final int NO_MAX_FD_COUNT = -1; private static volatile HardwareConfigState instance; private final int sdkBasedMaxFdCount; @GuardedBy("this") private int decodesSinceLastFdCheck; @GuardedBy("this") private boolean isFdSizeBelowHardwareLimit = true; /** * Only mutated on the main thread. Read by any number of background threads concurrently. * *

    Defaults to {@code false} because we need to wait for the GL context to be initialized and * it defaults to not initialized (https://b.corp.google.com/issues/126573603#comment12). */ private final AtomicBoolean isHardwareConfigAllowedByAppState = new AtomicBoolean(false); public static HardwareConfigState getInstance() { if (instance == null) { synchronized (HardwareConfigState.class) { if (instance == null) { instance = new HardwareConfigState(); } } } return instance; } @VisibleForTesting HardwareConfigState() { sdkBasedMaxFdCount = MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_P; } public void blockHardwareBitmaps() { Util.assertMainThread(); isHardwareConfigAllowedByAppState.set(false); } public void unblockHardwareBitmaps() { Util.assertMainThread(); isHardwareConfigAllowedByAppState.set(true); } public boolean isHardwareConfigAllowed( int targetWidth, int targetHeight, boolean isHardwareConfigAllowed, boolean isExifOrientationRequired) { if (!isHardwareConfigAllowed) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Hardware config disallowed by caller"); } return false; } if (!HARDWARE_BITMAPS_SUPPORTED) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Hardware config disallowed by sdk"); } return false; } if (areHardwareBitmapsBlockedByAppState()) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Hardware config disallowed by app state"); } return false; } if (isExifOrientationRequired) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Hardware config disallowed because exif orientation is required"); } return false; } if (targetWidth < 0 || targetHeight < 0) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Hardware config disallowed because of invalid dimensions"); } return false; } // Make sure to call isFdSizeBelowHardwareLimit last because it has side affects. if (!isFdSizeBelowHardwareLimit()) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Hardware config disallowed because there are insufficient FDs"); } return false; } return true; } private boolean areHardwareBitmapsBlockedByAppState() { return BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED && !isHardwareConfigAllowedByAppState.get(); } @TargetApi(Build.VERSION_CODES.O) boolean setHardwareConfigIfAllowed( int targetWidth, int targetHeight, BitmapFactory.Options optionsWithScaling, boolean isHardwareConfigAllowed, boolean isExifOrientationRequired) { boolean result = isHardwareConfigAllowed( targetWidth, targetHeight, isHardwareConfigAllowed, isExifOrientationRequired); if (result) { optionsWithScaling.inPreferredConfig = Bitmap.Config.HARDWARE; optionsWithScaling.inMutable = false; } return result; } private static boolean isHardwareBitmapCountReducedOnApi28ByB139097735() { if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) { return false; } for (String prefixOrModelName : Arrays.asList( "GM1900", "GM1901", "GM1903", "GM1911", "GM1915", "ONEPLUS A3000", "ONEPLUS A3010", "ONEPLUS A5010", "ONEPLUS A5000", "ONEPLUS A3003", "ONEPLUS A6000", "ONEPLUS A6003", "ONEPLUS A6010", "ONEPLUS A6013")) { if (Build.MODEL.startsWith(prefixOrModelName)) { return true; } } return false; } private int getMaxFdCount() { if (isHardwareBitmapCountReducedOnApi28ByB139097735()) { return REDUCED_MAX_FDS_FOR_HARDWARE_CONFIGS_P; } return sdkBasedMaxFdCount; } private synchronized boolean isFdSizeBelowHardwareLimit() { if (++decodesSinceLastFdCheck >= MINIMUM_DECODES_BETWEEN_FD_CHECKS) { decodesSinceLastFdCheck = 0; int currentFds = FD_SIZE_LIST.list().length; long maxFdCount = getMaxFdCount(); isFdSizeBelowHardwareLimit = currentFds < maxFdCount; if (!isFdSizeBelowHardwareLimit && Log.isLoggable(Downsampler.TAG, Log.WARN)) { Log.w( Downsampler.TAG, "Excluding HARDWARE bitmap config because we're over the file descriptor limit" + ", file descriptors " + currentFds + ", limit " + maxFdCount); } } return isFdSizeBelowHardwareLimit; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/ImageReader.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.os.ParcelFileDescriptor; import androidx.annotation.Nullable; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.load.data.InputStreamRewinder; import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.util.ByteBufferUtil; import com.bumptech.glide.util.Preconditions; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.List; /** * This is a helper class for {@link Downsampler} that abstracts out image operations from the input * type wrapped into a {@link DataRewinder}. */ interface ImageReader { @Nullable Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException; ImageHeaderParser.ImageType getImageType() throws IOException; int getImageOrientation() throws IOException; boolean hasJpegMpf() throws IOException; void stopGrowingBuffers(); final class ByteArrayReader implements ImageReader { private final byte[] bytes; private final List parsers; private final ArrayPool byteArrayPool; ByteArrayReader(byte[] bytes, List parsers, ArrayPool byteArrayPool) { this.bytes = bytes; this.parsers = parsers; this.byteArrayPool = byteArrayPool; } @Nullable @Override public Bitmap decodeBitmap(Options options) { return GlideBitmapFactory.decodeByteArray(bytes, options, this); } @Override public ImageType getImageType() throws IOException { return ImageHeaderParserUtils.getType(parsers, ByteBuffer.wrap(bytes)); } @Override public int getImageOrientation() throws IOException { return ImageHeaderParserUtils.getOrientation(parsers, ByteBuffer.wrap(bytes), byteArrayPool); } @Override public boolean hasJpegMpf() throws IOException { return ImageHeaderParserUtils.hasJpegMpf(parsers, ByteBuffer.wrap(bytes), byteArrayPool); } @Override public void stopGrowingBuffers() {} } final class FileReader implements ImageReader { private final File file; private final List parsers; private final ArrayPool byteArrayPool; FileReader(File file, List parsers, ArrayPool byteArrayPool) { this.file = file; this.parsers = parsers; this.byteArrayPool = byteArrayPool; } @Nullable @Override public Bitmap decodeBitmap(Options options) throws FileNotFoundException { InputStream is = null; try { is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool); return GlideBitmapFactory.decodeStream(is, options, this); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } } } @Override public ImageType getImageType() throws IOException { InputStream is = null; try { is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool); return ImageHeaderParserUtils.getType(parsers, is, byteArrayPool); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } } } @Override public int getImageOrientation() throws IOException { InputStream is = null; try { is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool); return ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } } } @Override public boolean hasJpegMpf() throws IOException { InputStream is = null; try { is = new FileInputStream(file); return ImageHeaderParserUtils.hasJpegMpf(parsers, is, byteArrayPool); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Ignored. } } } } @Override public void stopGrowingBuffers() {} } final class ByteBufferReader implements ImageReader { private final ByteBuffer buffer; private final List parsers; private final ArrayPool byteArrayPool; ByteBufferReader(ByteBuffer buffer, List parsers, ArrayPool byteArrayPool) { this.buffer = buffer; this.parsers = parsers; this.byteArrayPool = byteArrayPool; } @Nullable @Override public Bitmap decodeBitmap(Options options) { InputStream inputStream = stream(); return GlideBitmapFactory.decodeStream(inputStream, options, this); } @Override public ImageType getImageType() throws IOException { return ImageHeaderParserUtils.getType(parsers, ByteBufferUtil.rewind(buffer)); } @Override public int getImageOrientation() throws IOException { return ImageHeaderParserUtils.getOrientation( parsers, ByteBufferUtil.rewind(buffer), byteArrayPool); } @Override public boolean hasJpegMpf() throws IOException { return ImageHeaderParserUtils.hasJpegMpf( parsers, ByteBufferUtil.rewind(buffer), byteArrayPool); } @Override public void stopGrowingBuffers() {} private InputStream stream() { return ByteBufferUtil.toStream(ByteBufferUtil.rewind(buffer)); } } final class InputStreamImageReader implements ImageReader { private final InputStreamRewinder dataRewinder; private final ArrayPool byteArrayPool; private final List parsers; InputStreamImageReader( InputStream is, List parsers, ArrayPool byteArrayPool) { this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool); this.parsers = Preconditions.checkNotNull(parsers); dataRewinder = new InputStreamRewinder(is, byteArrayPool); } @Nullable @Override public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException { InputStream inputStream = dataRewinder.rewindAndGet(); return GlideBitmapFactory.decodeStream(inputStream, options, this); } @Override public ImageHeaderParser.ImageType getImageType() throws IOException { return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool); } @Override public int getImageOrientation() throws IOException { return ImageHeaderParserUtils.getOrientation( parsers, dataRewinder.rewindAndGet(), byteArrayPool); } @Override public boolean hasJpegMpf() throws IOException { return ImageHeaderParserUtils.hasJpegMpf(parsers, dataRewinder.rewindAndGet(), byteArrayPool); } @Override public void stopGrowingBuffers() { dataRewinder.fixMarkLimits(); } } final class ParcelFileDescriptorImageReader implements ImageReader { private final ArrayPool byteArrayPool; private final List parsers; private final ParcelFileDescriptorRewinder dataRewinder; ParcelFileDescriptorImageReader( ParcelFileDescriptor parcelFileDescriptor, List parsers, ArrayPool byteArrayPool) { this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool); this.parsers = Preconditions.checkNotNull(parsers); dataRewinder = new ParcelFileDescriptorRewinder(parcelFileDescriptor); } @Nullable @Override public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException { ParcelFileDescriptor parcelFileDescriptor = dataRewinder.rewindAndGet(); FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); return GlideBitmapFactory.decodeFileDescriptor(fileDescriptor, options, this); } @Override public ImageHeaderParser.ImageType getImageType() throws IOException { return ImageHeaderParserUtils.getType(parsers, dataRewinder, byteArrayPool); } @Override public int getImageOrientation() throws IOException { return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder, byteArrayPool); } @Override public boolean hasJpegMpf() throws IOException { return ImageHeaderParserUtils.hasJpegMpf(parsers, dataRewinder, byteArrayPool); } @Override public void stopGrowingBuffers() { // Nothing to do here. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/InputStreamBitmapImageDecoderResourceDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.Source; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.ByteBufferUtil; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; /** {@link InputStream} specific implementation of {@link BitmapImageDecoderResourceDecoder}. */ @RequiresApi(api = 28) public final class InputStreamBitmapImageDecoderResourceDecoder implements ResourceDecoder { private final BitmapImageDecoderResourceDecoder wrapped = new BitmapImageDecoderResourceDecoder(); @Override public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException { return true; } @Override public Resource decode( @NonNull InputStream stream, int width, int height, @NonNull Options options) throws IOException { ByteBuffer buffer = ByteBufferUtil.fromStream(stream); Source source = ImageDecoder.createSource(buffer); return wrapped.decode(source, width, height, options); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/LazyBitmapDrawableResource.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.Initializable; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; /** * Lazily allocates a {@link android.graphics.drawable.BitmapDrawable} from a given {@link * android.graphics.Bitmap} on the first call to {@link #get()}. */ public final class LazyBitmapDrawableResource implements Resource, Initializable { private final Resources resources; private final Resource bitmapResource; /** * @deprecated Use {@link #obtain(Resources, Resource)} instead, it can be unsafe to extract * {@link Bitmap}s from their wrapped {@link Resource}. */ @Deprecated public static LazyBitmapDrawableResource obtain(Context context, Bitmap bitmap) { return (LazyBitmapDrawableResource) obtain( context.getResources(), BitmapResource.obtain(bitmap, Glide.get(context).getBitmapPool())); } /** * @deprecated Use {@link #obtain(Resources, Resource)} instead, it can be unsafe to extract * {@link Bitmap}s from their wrapped {@link Resource}. */ @Deprecated public static LazyBitmapDrawableResource obtain( Resources resources, BitmapPool bitmapPool, Bitmap bitmap) { return (LazyBitmapDrawableResource) obtain(resources, BitmapResource.obtain(bitmap, bitmapPool)); } @Nullable public static Resource obtain( @NonNull Resources resources, @Nullable Resource bitmapResource) { if (bitmapResource == null) { return null; } return new LazyBitmapDrawableResource(resources, bitmapResource); } private LazyBitmapDrawableResource( @NonNull Resources resources, @NonNull Resource bitmapResource) { this.resources = Preconditions.checkNotNull(resources); this.bitmapResource = Preconditions.checkNotNull(bitmapResource); } @NonNull @Override public Class getResourceClass() { return BitmapDrawable.class; } @NonNull @Override public BitmapDrawable get() { return new BitmapDrawable(resources, bitmapResource.get()); } @Override public int getSize() { return bitmapResource.getSize(); } @Override public void recycle() { bitmapResource.recycle(); } @Override public void initialize() { if (bitmapResource instanceof Initializable) { ((Initializable) bitmapResource).initialize(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/ParcelFileDescriptorBitmapDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.os.Build; import android.os.ParcelFileDescriptor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import java.io.IOException; /** Decodes {@link Bitmap}s from {@link ParcelFileDescriptor}s. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public final class ParcelFileDescriptorBitmapDecoder implements ResourceDecoder { // 512MB. While I don't have data on the number of valid image files > 512mb, I have determined // that virtually all crashes related to Huawei/Honor's DRM checker go away when we don't attempt // to decode files larger than this. We could increase this to 1GB safely, but it seems like 512MB // might be a little better from a crash reduction perspective. See b/201464175. private static final int MAXIMUM_FILE_BYTE_SIZE_FOR_FILE_DESCRIPTOR_DECODER = 512 * 1024 * 1024; private final Downsampler downsampler; public ParcelFileDescriptorBitmapDecoder(Downsampler downsampler) { this.downsampler = downsampler; } @Override public boolean handles(@NonNull ParcelFileDescriptor source, @NonNull Options options) { return isSafeToTryDecoding(source) && downsampler.handles(source); } private boolean isSafeToTryDecoding(@NonNull ParcelFileDescriptor source) { if ("HUAWEI".equalsIgnoreCase(Build.MANUFACTURER) || "HONOR".equalsIgnoreCase(Build.MANUFACTURER)) { return source.getStatSize() <= MAXIMUM_FILE_BYTE_SIZE_FOR_FILE_DESCRIPTOR_DECODER; } return true; } @Nullable @Override public Resource decode( @NonNull ParcelFileDescriptor source, int width, int height, @NonNull Options options) throws IOException { return downsampler.decode(source, width, height, options); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/RecyclableBufferedInputStream.java ================================================ package com.bumptech.glide.load.resource.bitmap; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * Wraps an existing {@link InputStream} and buffers the input. Expensive interaction with * the underlying input stream is minimized, since most (smaller) requests can be satisfied by * accessing the buffer alone. The drawback is that some extra space is required to hold the buffer * and that copying takes place when filling that buffer, but this is usually outweighed by the * performance benefits. * *

    A typical application pattern for the class looks like this: * *

     * BufferedInputStream buf = new BufferedInputStream(new FileInputStream("file.java"));
     * 
    */ public class RecyclableBufferedInputStream extends FilterInputStream { /** The buffer containing the current bytes read from the target InputStream. */ private volatile byte[] buf; /** The total number of bytes inside the byte array {@code buf}. */ private int count; /** The current limit, which when passed, invalidates the current mark. */ private int marklimit; /** * The currently marked position. -1 indicates no mark has been put or the mark has been * invalidated. */ private int markpos = -1; /** The current position within the byte array {@code buf}. */ private int pos; private final ArrayPool byteArrayPool; public RecyclableBufferedInputStream(@NonNull InputStream in, @NonNull ArrayPool byteArrayPool) { this(in, byteArrayPool, ArrayPool.STANDARD_BUFFER_SIZE_BYTES); } @VisibleForTesting RecyclableBufferedInputStream( @NonNull InputStream in, @NonNull ArrayPool byteArrayPool, int bufferSize) { super(in); this.byteArrayPool = byteArrayPool; buf = byteArrayPool.get(bufferSize, byte[].class); } /** * Returns an estimated number of bytes that can be read or skipped without blocking for more * input. This method returns the number of bytes available in the buffer plus those available in * the source stream, but see {@link InputStream#available} for important caveats. * * @return the estimated number of bytes available * @throws IOException if this stream is closed or an error occurs */ @Override public synchronized int available() throws IOException { // in could be invalidated by close(). InputStream localIn = in; if (buf == null || localIn == null) { throw streamClosed(); } return count - pos + localIn.available(); } private static IOException streamClosed() throws IOException { throw new IOException("BufferedInputStream is closed"); } /** * Reduces the mark limit to match the current buffer length to prevent the buffer from continuing * to increase in size. * *

    Subsequent calls to {@link #mark(int)} will be obeyed and may cause the buffer size to * increase. */ // Public API. @SuppressWarnings("WeakerAccess") public synchronized void fixMarkLimit() { marklimit = buf.length; } public synchronized void release() { if (buf != null) { byteArrayPool.put(buf); buf = null; } } /** * Closes this stream. The source stream is closed and any resources associated with it are * released. * * @throws IOException if an error occurs while closing this stream. */ @Override public void close() throws IOException { if (buf != null) { byteArrayPool.put(buf); buf = null; } InputStream localIn = in; in = null; if (localIn != null) { localIn.close(); } } private int fillbuf(InputStream localIn, byte[] localBuf) throws IOException { if (markpos == -1 || pos - markpos >= marklimit) { // Mark position not put or exceeded readlimit int result = localIn.read(localBuf); if (result > 0) { markpos = -1; pos = 0; count = result; } return result; } // Added count == localBuf.length so that we do not immediately double the buffer size before // reading any data // when marklimit > localBuf.length. Instead, we will double the buffer size only after // reading the initial // localBuf worth of data without finding what we're looking for in the stream. This allows // us to put a // relatively small initial buffer size and a large marklimit for safety without causing an // allocation each time // read is called. if (markpos == 0 && marklimit > localBuf.length && count == localBuf.length) { // Increase buffer size to accommodate the readlimit int newLength = localBuf.length * 2; if (newLength > marklimit) { newLength = marklimit; } byte[] newbuf = byteArrayPool.get(newLength, byte[].class); System.arraycopy(localBuf, 0, newbuf, 0, localBuf.length); byte[] oldbuf = localBuf; // Reassign buf, which will invalidate any local references // FIXME: what if buf was null? localBuf = buf = newbuf; byteArrayPool.put(oldbuf); } else if (markpos > 0) { System.arraycopy(localBuf, markpos, localBuf, 0, localBuf.length - markpos); } // Set the new position and mark position pos -= markpos; count = markpos = 0; int bytesread = localIn.read(localBuf, pos, localBuf.length - pos); count = bytesread <= 0 ? pos : pos + bytesread; return bytesread; } /** * Sets a mark position in this stream. The parameter {@code readlimit} indicates how many bytes * can be read before a mark is invalidated. Calling {@link #reset()} will reposition the stream * back to the marked position if {@code readlimit} has not been surpassed. The underlying buffer * may be increased in size to allow {@code readlimit} number of bytes to be supported. * * @param readlimit the number of bytes that can be read before the mark is invalidated. * @see #reset() */ @Override public synchronized void mark(int readlimit) { // This is stupid, but BitmapFactory.decodeStream calls mark(1024) // which is too small for a substantial portion of images. This // change (using Math.max) ensures that we don't overwrite readlimit // with a smaller value marklimit = Math.max(marklimit, readlimit); markpos = pos; } /** * Indicates whether {@code BufferedInputStream} supports the {@link #mark(int)} and {@link * #reset()} methods. * * @return {@code true} for BufferedInputStreams. * @see #mark(int) * @see #reset() */ @Override public boolean markSupported() { return true; } /** * Reads a single byte from this stream and returns it as an integer in the range from 0 to 255. * Returns -1 if the end of the source string has been reached. If the internal buffer does not * contain any available bytes then it is filled from the source stream and the first byte is * returned. * * @return the byte read or -1 if the end of the source stream has been reached. * @throws IOException if this stream is closed or another IOException occurs. */ @Override public synchronized int read() throws IOException { // Use local refs since buf and in may be invalidated by an // unsynchronized close() byte[] localBuf = buf; InputStream localIn = in; if (localBuf == null || localIn == null) { throw streamClosed(); } // Are there buffered bytes available? if (pos >= count && fillbuf(localIn, localBuf) == -1) { // no, fill buffer return -1; } // localBuf may have been invalidated by fillbuf if (localBuf != buf) { localBuf = buf; if (localBuf == null) { throw streamClosed(); } } // Did filling the buffer fail with -1 (EOF)? if (count - pos > 0) { return localBuf[pos++] & 0xFF; } return -1; } /** * Reads at most {@code byteCount} bytes from this stream and stores them in byte array {@code * buffer} starting at offset {@code offset}. Returns the number of bytes actually read or -1 if * no bytes were read and the end of the stream was encountered. If all the buffered bytes have * been used, a mark has not been put and the requested number of bytes is larger than the * receiver's buffer size, this implementation bypasses the buffer and simply places the results * directly into {@code buffer}. * * @param buffer the byte array in which to store the bytes read. * @return the number of bytes actually read or -1 if end of stream. * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code byteCount < 0}, or if {@code * offset + byteCount} is greater than the size of {@code buffer}. * @throws IOException if the stream is already closed or another IOException occurs. */ @Override public synchronized int read(@NonNull byte[] buffer, int offset, int byteCount) throws IOException { // Use local ref since buf may be invalidated by an unsynchronized close() byte[] localBuf = buf; if (localBuf == null) { throw streamClosed(); } // Arrays.checkOffsetAndCount(buffer.length, offset, byteCount); if (byteCount == 0) { return 0; } InputStream localIn = in; if (localIn == null) { throw streamClosed(); } int required; if (pos < count) { // There are bytes available in the buffer. int copylength = count - pos >= byteCount ? byteCount : count - pos; System.arraycopy(localBuf, pos, buffer, offset, copylength); pos += copylength; if (copylength == byteCount || localIn.available() == 0) { return copylength; } offset += copylength; required = byteCount - copylength; } else { required = byteCount; } while (true) { int read; // If we're not marked and the required size is greater than the buffer, // simply read the bytes directly bypassing the buffer. if (markpos == -1 && required >= localBuf.length) { read = localIn.read(buffer, offset, required); if (read == -1) { return required == byteCount ? -1 : byteCount - required; } } else { if (fillbuf(localIn, localBuf) == -1) { return required == byteCount ? -1 : byteCount - required; } // localBuf may have been invalidated by fillbuf if (localBuf != buf) { localBuf = buf; if (localBuf == null) { throw streamClosed(); } } read = count - pos >= required ? required : count - pos; System.arraycopy(localBuf, pos, buffer, offset, read); pos += read; } required -= read; if (required == 0) { return byteCount; } if (localIn.available() == 0) { return byteCount - required; } offset += read; } } /** * Resets this stream to the last marked location. * * @throws IOException if this stream is closed, no mark has been put or the mark is no longer * valid because more than {@code readlimit} bytes have been read since setting the mark. * @see #mark(int) */ @Override public synchronized void reset() throws IOException { if (buf == null) { throw new IOException("Stream is closed"); } if (-1 == markpos) { throw new InvalidMarkException( "Mark has been invalidated, pos: " + pos + " markLimit: " + marklimit); } pos = markpos; } /** * Skips {@code byteCount} bytes in this stream. Subsequent calls to {@link #read} will not return * these bytes unless {@link #reset} is used. * * @param byteCount the number of bytes to skip. This method does nothing and returns 0 if {@code * byteCount} is less than zero. * @return the number of bytes actually skipped. * @throws IOException if this stream is closed or another IOException occurs. */ @Override public synchronized long skip(long byteCount) throws IOException { if (byteCount < 1) { return 0; } // Use local refs since buf and in may be invalidated by an unsynchronized close() byte[] localBuf = buf; if (localBuf == null) { throw streamClosed(); } InputStream localIn = in; if (localIn == null) { throw streamClosed(); } if (count - pos >= byteCount) { pos = (int) (pos + byteCount); return byteCount; } // See https://errorprone.info/bugpattern/IntLongMath. long read = (long) count - pos; pos = count; if (markpos != -1 && byteCount <= marklimit) { if (fillbuf(localIn, localBuf) == -1) { return read; } if (count - pos >= byteCount - read) { // See https://errorprone.info/bugpattern/NarrowingCompoundAssignment. pos = (int) (pos + byteCount - read); return byteCount; } // Couldn't get all the bytes, skip what we read. read = read + count - pos; pos = count; return read; } // We can't skip over the remaining bytes without exceeding the mark limit so there will be no // way to reset to a proper position in the stream. long skipped = localIn.skip(byteCount - read); if (skipped > 0) { markpos = -1; } return read + skipped; } /** * An exception thrown when a mark can no longer be obeyed because the underlying buffer size is * smaller than the amount of data read after the mark position. */ static class InvalidMarkException extends IOException { private static final long serialVersionUID = -4338378848813561757L; InvalidMarkException(String detailMessage) { super(detailMessage); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/ResourceBitmapDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.drawable.ResourceDrawableDecoder; import com.bumptech.glide.request.target.Target; /** * Decodes {@link Bitmap}s from resource ids. * *

    The framework will decode some resources as {@link Drawable}s that do not wrap {@link * Bitmap}s. This decoder will attempt to return a {@link Bitmap} for those {@link Drawable}s anyway * by drawing the {@link Drawable} to a {@link Canvas}s using the {@link Drawable}'s intrinsic * bounds or the dimensions provided to {@link #decode(Uri, int, int, Options)}. * *

    For non-{@link Bitmap} {@link Drawable}s that return {@code <= 0} for {@link * Drawable#getIntrinsicWidth()} and/or {@link Drawable#getIntrinsicHeight()}, this decoder will * fail if the width and height provided to {@link #decode(Uri, int, int, Options)} are {@link * Target#SIZE_ORIGINAL}. */ public class ResourceBitmapDecoder implements ResourceDecoder { private final ResourceDrawableDecoder drawableDecoder; private final BitmapPool bitmapPool; public ResourceBitmapDecoder(ResourceDrawableDecoder drawableDecoder, BitmapPool bitmapPool) { this.drawableDecoder = drawableDecoder; this.bitmapPool = bitmapPool; } @Override public boolean handles(@NonNull Uri source, @NonNull Options options) { return ContentResolver.SCHEME_ANDROID_RESOURCE.equals(source.getScheme()); } @Nullable @Override public Resource decode( @NonNull Uri source, int width, int height, @NonNull Options options) { Resource drawableResource = drawableDecoder.decode(source, width, height, options); if (drawableResource == null) { return null; } Drawable drawable = drawableResource.get(); return DrawableToBitmapConverter.convert(bitmapPool, drawable, width, height); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/Rotate.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Util; import java.nio.ByteBuffer; import java.security.MessageDigest; /** A {@link BitmapTransformation} which rotates the bitmap. */ public class Rotate extends BitmapTransformation { private static final String ID = "com.bumptech.glide.load.resource.bitmap.Rotate"; private static final byte[] ID_BYTES = ID.getBytes(CHARSET); private final int degreesToRotate; /** * @param degreesToRotate number of degrees to rotate the image by. If zero the original image is * not modified. */ public Rotate(int degreesToRotate) { this.degreesToRotate = degreesToRotate; } @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return TransformationUtils.rotateImage(toTransform, degreesToRotate); } @Override public boolean equals(Object o) { if (o instanceof Rotate) { Rotate other = (Rotate) o; return degreesToRotate == other.degreesToRotate; } return false; } @Override public int hashCode() { return Util.hashCode(ID.hashCode(), Util.hashCode(degreesToRotate)); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(ID_BYTES); byte[] degreesData = ByteBuffer.allocate(4).putInt(degreesToRotate).array(); messageDigest.update(degreesData); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/RoundedCorners.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; import java.nio.ByteBuffer; import java.security.MessageDigest; /** A {@link BitmapTransformation} which rounds the corners of a bitmap. */ public final class RoundedCorners extends BitmapTransformation { private static final String ID = "com.bumptech.glide.load.resource.bitmap.RoundedCorners"; private static final byte[] ID_BYTES = ID.getBytes(CHARSET); private final int roundingRadius; /** * @param roundingRadius the corner radius (in device-specific pixels). * @throws IllegalArgumentException if rounding radius is 0 or less. */ public RoundedCorners(int roundingRadius) { Preconditions.checkArgument(roundingRadius > 0, "roundingRadius must be greater than 0."); this.roundingRadius = roundingRadius; } @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return TransformationUtils.roundedCorners(pool, toTransform, roundingRadius); } @Override public boolean equals(Object o) { if (o instanceof RoundedCorners) { RoundedCorners other = (RoundedCorners) o; return roundingRadius == other.roundingRadius; } return false; } @Override public int hashCode() { return Util.hashCode(ID.hashCode(), Util.hashCode(roundingRadius)); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(ID_BYTES); byte[] radiusData = ByteBuffer.allocate(4).putInt(roundingRadius).array(); messageDigest.update(radiusData); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/StreamBitmapDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.ExceptionPassthroughInputStream; import com.bumptech.glide.util.MarkEnforcingInputStream; import java.io.IOException; import java.io.InputStream; /** * Decodes {@link android.graphics.Bitmap Bitmaps} from {@link java.io.InputStream InputStreams}. */ public class StreamBitmapDecoder implements ResourceDecoder { private final Downsampler downsampler; private final ArrayPool byteArrayPool; public StreamBitmapDecoder(Downsampler downsampler, ArrayPool byteArrayPool) { this.downsampler = downsampler; this.byteArrayPool = byteArrayPool; } @Override public boolean handles(@NonNull InputStream source, @NonNull Options options) { return downsampler.handles(source); } @Override public Resource decode( @NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException { // Use to fix the mark limit to avoid allocating buffers that fit entire images. final RecyclableBufferedInputStream bufferedStream; final boolean ownsBufferedStream; if (source instanceof RecyclableBufferedInputStream) { bufferedStream = (RecyclableBufferedInputStream) source; ownsBufferedStream = false; } else { bufferedStream = new RecyclableBufferedInputStream(source, byteArrayPool); ownsBufferedStream = true; } // Use to retrieve exceptions thrown while reading. // TODO(#126): when the framework no longer returns partially decoded Bitmaps or provides a // way to determine if a Bitmap is partially decoded, consider removing. ExceptionPassthroughInputStream exceptionStream = ExceptionPassthroughInputStream.obtain(bufferedStream); // Use to read data. // Ensures that we can always reset after reading an image header so that we can still // attempt to decode the full image even when the header decode fails and/or overflows our read // buffer. See #283. MarkEnforcingInputStream invalidatingStream = new MarkEnforcingInputStream(exceptionStream); UntrustedCallbacks callbacks = new UntrustedCallbacks(bufferedStream, exceptionStream); try { return downsampler.decode(invalidatingStream, width, height, options, callbacks); } finally { exceptionStream.release(); if (ownsBufferedStream) { bufferedStream.release(); } } } /** * Callbacks that provide reasonable handling for streams that may be unbuffered or insufficiently * buffered or that may throw exceptions during decoding. */ static class UntrustedCallbacks implements Downsampler.DecodeCallbacks { private final RecyclableBufferedInputStream bufferedStream; private final ExceptionPassthroughInputStream exceptionStream; UntrustedCallbacks( RecyclableBufferedInputStream bufferedStream, ExceptionPassthroughInputStream exceptionStream) { this.bufferedStream = bufferedStream; this.exceptionStream = exceptionStream; } @Override public void onObtainBounds() { // Once we've read the image header, we no longer need to allow the buffer to expand in // size. To avoid unnecessary allocations reading image data, we fix the mark limit so that it // is no larger than our current buffer size here. See issue #225. bufferedStream.fixMarkLimit(); } @Override public void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) throws IOException { // BitmapFactory swallows exceptions during decodes and in some cases when inBitmap is non // null, may catch and log a stack trace but still return a non null bitmap. To avoid // displaying partially decoded bitmaps, we catch exceptions reading from the stream in our // ExceptionCatchingInputStream and throw them here. IOException streamException = exceptionStream.getException(); if (streamException != null) { if (downsampled != null) { bitmapPool.put(downsampled); } throw streamException; } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/TransformationUtils.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.Shader; import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.exifinterface.media.ExifInterface; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** A class with methods to efficiently resize Bitmaps. */ // Legacy Public APIs. @SuppressWarnings("WeakerAccess") public final class TransformationUtils { private static final String TAG = "TransformationUtils"; public static final int PAINT_FLAGS = Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG; private static final Paint DEFAULT_PAINT = new Paint(PAINT_FLAGS); private static final int CIRCLE_CROP_PAINT_FLAGS = PAINT_FLAGS | Paint.ANTI_ALIAS_FLAG; private static final Paint CIRCLE_CROP_SHAPE_PAINT = new Paint(CIRCLE_CROP_PAINT_FLAGS); private static final Paint CIRCLE_CROP_BITMAP_PAINT; // See #738. private static final Set MODELS_REQUIRING_BITMAP_LOCK = new HashSet<>( Arrays.asList( // Moto X gen 2 "XT1085", "XT1092", "XT1093", "XT1094", "XT1095", "XT1096", "XT1097", "XT1098", // Moto G gen 1 "XT1031", "XT1028", "XT937C", "XT1032", "XT1008", "XT1033", "XT1035", "XT1034", "XT939G", "XT1039", "XT1040", "XT1042", "XT1045", // Moto G gen 2 "XT1063", "XT1064", "XT1068", "XT1069", "XT1072", "XT1077", "XT1078", "XT1079")); /** * https://github.com/bumptech/glide/issues/738 On some devices, bitmap drawing is not thread * safe. This lock only locks for these specific devices. For other types of devices the lock is * always available and therefore does not impact performance */ private static final Lock BITMAP_DRAWABLE_LOCK = MODELS_REQUIRING_BITMAP_LOCK.contains(Build.MODEL) ? new ReentrantLock() : new NoLock(); static { CIRCLE_CROP_BITMAP_PAINT = new Paint(CIRCLE_CROP_PAINT_FLAGS); CIRCLE_CROP_BITMAP_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); } private TransformationUtils() { // Utility class. } public static Lock getBitmapDrawableLock() { return BITMAP_DRAWABLE_LOCK; } /** * A potentially expensive operation to crop the given Bitmap so that it fills the given * dimensions. This operation is significantly less expensive in terms of memory if a mutable * Bitmap with the given dimensions is passed in as well. * * @param pool The BitmapPool to obtain a bitmap from. * @param inBitmap The Bitmap to resize. * @param width The width in pixels of the final Bitmap. * @param height The height in pixels of the final Bitmap. * @return The resized Bitmap (will be recycled if recycled is not null). */ public static Bitmap centerCrop( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width, int height) { if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) { return inBitmap; } // From ImageView/Bitmap.createScaledBitmap. final float scale; final float dx; final float dy; Matrix m = new Matrix(); if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) { scale = (float) height / (float) inBitmap.getHeight(); dx = (width - inBitmap.getWidth() * scale) * 0.5f; dy = 0; } else { scale = (float) width / (float) inBitmap.getWidth(); dx = 0; dy = (height - inBitmap.getHeight() * scale) * 0.5f; } m.setScale(scale, scale); m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap)); // We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given. TransformationUtils.setAlpha(inBitmap, result); applyMatrix(inBitmap, result, m); return result; } /** * An expensive operation to resize the given Bitmap down so that it fits within the given * dimensions maintain the original proportions. * * @param pool The BitmapPool obtain a bitmap from. * @param inBitmap The Bitmap to shrink. * @param width The width in pixels the final image will fit within. * @param height The height in pixels the final image will fit within. * @return A new Bitmap shrunk to fit within the given dimensions, or toFit if toFit's width or * height matches the given dimensions and toFit fits within the given dimensions */ public static Bitmap fitCenter( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width, int height) { if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "requested target size matches input, returning input"); } return inBitmap; } final float widthPercentage = width / (float) inBitmap.getWidth(); final float heightPercentage = height / (float) inBitmap.getHeight(); final float minPercentage = Math.min(widthPercentage, heightPercentage); // Round here in case we've decoded exactly the image we want, but take the floor below to // avoid a line of garbage or blank pixels in images. int targetWidth = Math.round(minPercentage * inBitmap.getWidth()); int targetHeight = Math.round(minPercentage * inBitmap.getHeight()); if (inBitmap.getWidth() == targetWidth && inBitmap.getHeight() == targetHeight) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "adjusted target size matches input, returning input"); } return inBitmap; } // Take the floor of the target width/height, not round. If the matrix // passed into drawBitmap rounds differently, we want to slightly // overdraw, not underdraw, to avoid artifacts from bitmap reuse. targetWidth = (int) (minPercentage * inBitmap.getWidth()); targetHeight = (int) (minPercentage * inBitmap.getHeight()); Bitmap.Config config = getNonNullConfig(inBitmap); Bitmap toReuse = pool.get(targetWidth, targetHeight, config); // We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given. TransformationUtils.setAlpha(inBitmap, toReuse); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "request: " + width + "x" + height); Log.v(TAG, "toFit: " + inBitmap.getWidth() + "x" + inBitmap.getHeight()); Log.v(TAG, "toReuse: " + toReuse.getWidth() + "x" + toReuse.getHeight()); Log.v(TAG, "minPct: " + minPercentage); } Matrix matrix = new Matrix(); matrix.setScale(minPercentage, minPercentage); applyMatrix(inBitmap, toReuse, matrix); return toReuse; } /** * If the Bitmap is smaller or equal to the Target it returns the original size, if not then * {@link #fitCenter(BitmapPool, Bitmap, int, int)} is called instead. * * @param pool The BitmapPool obtain a bitmap from. * @param inBitmap The Bitmap to center. * @param width The width in pixels of the target. * @param height The height in pixels of the target. * @return returns input Bitmap if smaller or equal to target, or toFit if the Bitmap's width or * height is larger than the given dimensions */ public static Bitmap centerInside( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width, int height) { if (inBitmap.getWidth() <= width && inBitmap.getHeight() <= height) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "requested target size larger or equal to input, returning input"); } return inBitmap; } else { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "requested target size too big for input, fit centering instead"); } return fitCenter(pool, inBitmap, width, height); } } /** * Sets the alpha of the Bitmap we're going to re-use to the alpha of the Bitmap we're going to * transform. This keeps {@link android.graphics.Bitmap#hasAlpha()}} consistent before and after * the transformation for transformations that don't add or remove transparent pixels. * * @param inBitmap The {@link android.graphics.Bitmap} that will be transformed. * @param outBitmap The {@link android.graphics.Bitmap} that will be returned from the * transformation. */ public static void setAlpha(Bitmap inBitmap, Bitmap outBitmap) { outBitmap.setHasAlpha(inBitmap.hasAlpha()); } /** * This is an expensive operation that copies the image in place with the pixels rotated. If * possible rather use getOrientationMatrix, and put that as the imageMatrix on an ImageView. * * @param imageToOrient Image Bitmap to orient. * @param degreesToRotate number of degrees to rotate the image by. If zero the original image is * returned unmodified. * @return The oriented bitmap. May be the imageToOrient without modification, or a new Bitmap. */ public static Bitmap rotateImage(@NonNull Bitmap imageToOrient, int degreesToRotate) { Bitmap result = imageToOrient; try { if (degreesToRotate != 0) { Matrix matrix = new Matrix(); matrix.setRotate(degreesToRotate); result = Bitmap.createBitmap( imageToOrient, 0, 0, imageToOrient.getWidth(), imageToOrient.getHeight(), matrix, true /*filter*/); } } catch (Exception e) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Exception when trying to orient image", e); } } return result; } /** * Get the # of degrees an image must be rotated to match the given exif orientation. * * @param exifOrientation The exif orientation [1-8] * @return the number of degrees to rotate */ public static int getExifOrientationDegrees(int exifOrientation) { final int degreesToRotate; switch (exifOrientation) { case ExifInterface.ORIENTATION_TRANSPOSE: case ExifInterface.ORIENTATION_ROTATE_90: degreesToRotate = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: case ExifInterface.ORIENTATION_FLIP_VERTICAL: degreesToRotate = 180; break; case ExifInterface.ORIENTATION_TRANSVERSE: case ExifInterface.ORIENTATION_ROTATE_270: degreesToRotate = 270; break; default: degreesToRotate = 0; break; } return degreesToRotate; } /** * Rotate and/or flip the image to match the given exif orientation. * * @param pool A pool that may or may not contain an image of the necessary dimensions. * @param inBitmap The bitmap to rotate/flip. * @param exifOrientation the exif orientation [1-8]. * @return The rotated and/or flipped image or toOrient if no rotation or flip was necessary. */ public static Bitmap rotateImageExif( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int exifOrientation) { if (!isExifOrientationRequired(exifOrientation)) { return inBitmap; } final Matrix matrix = new Matrix(); initializeMatrixForRotation(exifOrientation, matrix); // BitmapPool doesn't preserve gainmaps and color space, so use Bitmap.create to apply the // matrix. return Bitmap.createBitmap( inBitmap, /* x= */ 0, /* y= */ 0, inBitmap.getWidth(), inBitmap.getHeight(), matrix, /* filter= */ true); } /** * Returns {@code true} if the given exif orientation indicates that a transformation is necessary * and {@code false} otherwise. */ public static boolean isExifOrientationRequired(int exifOrientation) { switch (exifOrientation) { case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: case ExifInterface.ORIENTATION_ROTATE_180: case ExifInterface.ORIENTATION_FLIP_VERTICAL: case ExifInterface.ORIENTATION_TRANSPOSE: case ExifInterface.ORIENTATION_ROTATE_90: case ExifInterface.ORIENTATION_TRANSVERSE: case ExifInterface.ORIENTATION_ROTATE_270: return true; default: return false; } } /** * Crop the image to a circle and resize to the specified width/height. The circle crop will have * the same width and height equal to the min-edge of the result image. * * @param pool The BitmapPool obtain a bitmap from. * @param inBitmap The Bitmap to resize. * @param destWidth The width in pixels of the final Bitmap. * @param destHeight The height in pixels of the final Bitmap. * @return The resized Bitmap (will be recycled if recycled is not null). */ public static Bitmap circleCrop( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int destWidth, int destHeight) { int destMinEdge = Math.min(destWidth, destHeight); float radius = destMinEdge / 2f; int srcWidth = inBitmap.getWidth(); int srcHeight = inBitmap.getHeight(); float scaleX = destMinEdge / (float) srcWidth; float scaleY = destMinEdge / (float) srcHeight; float maxScale = Math.max(scaleX, scaleY); float scaledWidth = maxScale * srcWidth; float scaledHeight = maxScale * srcHeight; float left = (destMinEdge - scaledWidth) / 2f; float top = (destMinEdge - scaledHeight) / 2f; RectF destRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); // Alpha is required for this transformation. Bitmap toTransform = getAlphaSafeBitmap(pool, inBitmap); Bitmap.Config outConfig = getAlphaSafeConfig(inBitmap); Bitmap result = pool.get(destMinEdge, destMinEdge, outConfig); result.setHasAlpha(true); BITMAP_DRAWABLE_LOCK.lock(); try { Canvas canvas = new Canvas(result); // Draw a circle canvas.drawCircle(radius, radius, radius, CIRCLE_CROP_SHAPE_PAINT); // Draw the bitmap in the circle canvas.drawBitmap(toTransform, null, destRect, CIRCLE_CROP_BITMAP_PAINT); clear(canvas); } finally { BITMAP_DRAWABLE_LOCK.unlock(); } if (!toTransform.equals(inBitmap)) { pool.put(toTransform); } return result; } private static Bitmap getAlphaSafeBitmap( @NonNull BitmapPool pool, @NonNull Bitmap maybeAlphaSafe) { Bitmap.Config safeConfig = getAlphaSafeConfig(maybeAlphaSafe); if (safeConfig.equals(maybeAlphaSafe.getConfig())) { return maybeAlphaSafe; } Bitmap argbBitmap = pool.get(maybeAlphaSafe.getWidth(), maybeAlphaSafe.getHeight(), safeConfig); new Canvas(argbBitmap).drawBitmap(maybeAlphaSafe, 0 /*left*/, 0 /*top*/, null /*paint*/); // We now own this Bitmap. It's our responsibility to replace it in the pool outside this method // when we're finished with it. return argbBitmap; } @NonNull private static Config getAlphaSafeConfig(@NonNull Bitmap inBitmap) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Avoid short circuiting the sdk check. if (Bitmap.Config.RGBA_F16.equals(inBitmap.getConfig())) { // NOPMD return Bitmap.Config.RGBA_F16; } } return Bitmap.Config.ARGB_8888; } /** * Creates a bitmap from a source bitmap and rounds the corners. * * @param inBitmap the source bitmap to use as a basis for the created bitmap. * @param width the width of the generated bitmap. * @param height the height of the generated bitmap. * @param roundingRadius the corner radius to be applied (in device-specific pixels). * @return a {@link Bitmap} similar to inBitmap but with rounded corners. * @throws IllegalArgumentException if roundingRadius, width or height is 0 or less. * @deprecated Width and height are unused and ignored. Use {@link #roundedCorners(BitmapPool, * Bitmap, int)} instead. */ @Deprecated public static Bitmap roundedCorners( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, @SuppressWarnings("unused") int width, @SuppressWarnings("unused") int height, int roundingRadius) { return roundedCorners(pool, inBitmap, roundingRadius); } /** * Creates a bitmap from a source bitmap and rounds the corners. * *

    This method does NOT resize the given {@link Bitmap}, it only rounds it's corners. * To both resize and round the corners of an image, consider {@link * com.bumptech.glide.request.RequestOptions#transform(Transformation[])} and/or {@link * com.bumptech.glide.load.MultiTransformation}. * * @param inBitmap the source bitmap to use as a basis for the created bitmap. * @param roundingRadius the corner radius to be applied (in device-specific pixels). * @return a {@link Bitmap} similar to inBitmap but with rounded corners. * @throws IllegalArgumentException if roundingRadius, width or height is 0 or less. */ public static Bitmap roundedCorners( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, final int roundingRadius) { Preconditions.checkArgument(roundingRadius > 0, "roundingRadius must be greater than 0."); return roundedCorners( pool, inBitmap, new DrawRoundedCornerFn() { @Override public void drawRoundedCorners(Canvas canvas, Paint paint, RectF rect) { canvas.drawRoundRect(rect, roundingRadius, roundingRadius, paint); } }); } /** * Creates a bitmap from a source bitmap and rounds the corners, applying a potentially different * [X, Y] radius to each corner. * *

    This method does NOT resize the given {@link Bitmap}, it only rounds it's corners. * To both resize and round the corners of an image, consider {@link * com.bumptech.glide.request.RequestOptions#transform(Transformation[])} and/or {@link * com.bumptech.glide.load.MultiTransformation}. * * @param inBitmap the source bitmap to use as a basis for the created bitmap. * @param topLeft top-left radius * @param topRight top-right radius * @param bottomRight bottom-right radius * @param bottomLeft bottom-left radius * @return a {@link Bitmap} similar to inBitmap but with rounded corners. */ public static Bitmap roundedCorners( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, final float topLeft, final float topRight, final float bottomRight, final float bottomLeft) { return roundedCorners( pool, inBitmap, new DrawRoundedCornerFn() { @Override public void drawRoundedCorners(Canvas canvas, Paint paint, RectF rect) { Path path = new Path(); path.addRoundRect( rect, new float[] { topLeft, topLeft, topRight, topRight, bottomRight, bottomRight, bottomLeft, bottomLeft }, Path.Direction.CW); canvas.drawPath(path, paint); } }); } private static Bitmap roundedCorners( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, DrawRoundedCornerFn drawRoundedCornerFn) { // Alpha is required for this transformation. Bitmap.Config safeConfig = getAlphaSafeConfig(inBitmap); Bitmap toTransform = getAlphaSafeBitmap(pool, inBitmap); Bitmap result = pool.get(toTransform.getWidth(), toTransform.getHeight(), safeConfig); result.setHasAlpha(true); BitmapShader shader = new BitmapShader(toTransform, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setShader(shader); RectF rect = new RectF(0, 0, result.getWidth(), result.getHeight()); BITMAP_DRAWABLE_LOCK.lock(); try { Canvas canvas = new Canvas(result); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); drawRoundedCornerFn.drawRoundedCorners(canvas, paint, rect); clear(canvas); } finally { BITMAP_DRAWABLE_LOCK.unlock(); } if (!toTransform.equals(inBitmap)) { pool.put(toTransform); } return result; } // Avoids warnings in M+. private static void clear(Canvas canvas) { canvas.setBitmap(null); } @NonNull private static Bitmap.Config getNonNullConfig(@NonNull Bitmap bitmap) { return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888; } private static void applyMatrix( @NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap, Matrix matrix) { BITMAP_DRAWABLE_LOCK.lock(); try { Canvas canvas = new Canvas(targetBitmap); canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT); clear(canvas); } finally { BITMAP_DRAWABLE_LOCK.unlock(); } } @VisibleForTesting static void initializeMatrixForRotation(int exifOrientation, Matrix matrix) { switch (exifOrientation) { case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: matrix.setScale(-1, 1); break; case ExifInterface.ORIENTATION_ROTATE_180: matrix.setRotate(180); break; case ExifInterface.ORIENTATION_FLIP_VERTICAL: matrix.setRotate(180); matrix.postScale(-1, 1); break; case ExifInterface.ORIENTATION_TRANSPOSE: matrix.setRotate(90); matrix.postScale(-1, 1); break; case ExifInterface.ORIENTATION_ROTATE_90: matrix.setRotate(90); break; case ExifInterface.ORIENTATION_TRANSVERSE: matrix.setRotate(-90); matrix.postScale(-1, 1); break; case ExifInterface.ORIENTATION_ROTATE_270: matrix.setRotate(-90); break; default: // Do nothing. } } /** Convenience function for drawing a rounded bitmap. */ private interface DrawRoundedCornerFn { void drawRoundedCorners(Canvas canvas, Paint paint, RectF rect); } private static final class NoLock implements Lock { @Synthetic NoLock() {} @Override public void lock() { // do nothing } @Override public void lockInterruptibly() throws InterruptedException { // do nothing } @Override public boolean tryLock() { return true; } @Override public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException { return true; } @Override public void unlock() { // do nothing } @NonNull @Override public Condition newCondition() { throw new UnsupportedOperationException("Should not be called"); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/UnitBitmapDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.Util; /** * Passes through a (hopefully) non-owned {@link Bitmap} as a {@link Bitmap} based {@link Resource} * so that the given {@link Bitmap} is not recycled. */ public final class UnitBitmapDecoder implements ResourceDecoder { @Override public boolean handles(@NonNull Bitmap source, @NonNull Options options) { return true; } @Override public Resource decode( @NonNull Bitmap source, int width, int height, @NonNull Options options) { return new NonOwnedBitmapResource(source); } private static final class NonOwnedBitmapResource implements Resource { private final Bitmap bitmap; NonOwnedBitmapResource(@NonNull Bitmap bitmap) { this.bitmap = bitmap; } @NonNull @Override public Class getResourceClass() { return Bitmap.class; } @NonNull @Override public Bitmap get() { return bitmap; } @Override public int getSize() { return Util.getBitmapByteSize(bitmap); } @Override public void recycle() { // Do nothing. } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.content.Context; import android.os.ParcelFileDescriptor; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; /** * An {@link com.bumptech.glide.load.ResourceDecoder} that can decode a thumbnail frame {@link * android.graphics.Bitmap} from a {@link android.os.ParcelFileDescriptor} containing a video. * * @see android.media.MediaMetadataRetriever * @deprecated Use {@link VideoDecoder#parcel(BitmapPool)} instead. This class may be removed and * {@link VideoDecoder} may become final in a future version of Glide. */ @Deprecated public class VideoBitmapDecoder extends VideoDecoder { @SuppressWarnings("unused") public VideoBitmapDecoder(Context context) { this(Glide.get(context).getBitmapPool()); } // Public API @SuppressWarnings("WeakerAccess") public VideoBitmapDecoder(BitmapPool bitmapPool) { super(bitmapPool, new ParcelFileDescriptorInitializer()); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoDecoder.java ================================================ package com.bumptech.glide.load.resource.bitmap; import android.annotation.TargetApi; import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; import android.graphics.Matrix; import android.media.MediaDataSource; import android.media.MediaExtractor; import android.media.MediaFormat; import android.media.MediaMetadataRetriever; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.ParcelFileDescriptor; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.request.target.Target; import java.io.IOException; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Decodes video data to Bitmaps from {@link ParcelFileDescriptor}s and {@link * AssetFileDescriptor}s. * * @param The type of data, currently either {@link ParcelFileDescriptor} or {@link * AssetFileDescriptor}. */ public class VideoDecoder implements ResourceDecoder { private static final String TAG = "VideoDecoder"; /** * A constant indicating we should use whatever frame we consider best, frequently not the first * frame. */ public static final long DEFAULT_FRAME = -1; /** Matches the behavior of {@link MediaMetadataRetriever#getFrameAtTime(long)}. */ @VisibleForTesting static final int DEFAULT_FRAME_OPTION = MediaMetadataRetriever.OPTION_CLOSEST_SYNC; /** * A long indicating the time position (in microseconds) of the target frame which will be * retrieved. {@link android.media.MediaMetadataRetriever#getFrameAtTime(long)} is used to extract * the video frame. * *

    When retrieving the frame at the given time position, there is no guarantee that the data * source has a frame located at the position. When this happens, a frame nearby will be returned. * If the long is negative, time position and option will ignored, and any frame that the * implementation considers as representative may be returned. */ public static final Option TARGET_FRAME = Option.disk( "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.TargetFrame", DEFAULT_FRAME, new Option.CacheKeyUpdater() { private final ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); @Override public void update( @NonNull byte[] keyBytes, @NonNull Long value, @NonNull MessageDigest messageDigest) { messageDigest.update(keyBytes); synchronized (buffer) { buffer.position(0); messageDigest.update(buffer.putLong(value).array()); } } }); /** * An integer indicating the frame option used to retrieve a target frame. * *

    This option will be ignored if {@link #TARGET_FRAME} is not set or is set to {@link * #DEFAULT_FRAME}. * * @see MediaMetadataRetriever#getFrameAtTime(long, int) */ // Public API. @SuppressWarnings("WeakerAccess") public static final Option FRAME_OPTION = Option.disk( "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.FrameOption", /* defaultValue= */ MediaMetadataRetriever.OPTION_CLOSEST_SYNC, new Option.CacheKeyUpdater() { private final ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE); @Override public void update( @NonNull byte[] keyBytes, @NonNull Integer value, @NonNull MessageDigest messageDigest) { //noinspection ConstantConditions public API, people could have been doing it wrong if (value == null) { return; } messageDigest.update(keyBytes); synchronized (buffer) { buffer.position(0); messageDigest.update(buffer.putInt(value).array()); } } }); private static final MediaMetadataRetrieverFactory DEFAULT_FACTORY = new MediaMetadataRetrieverFactory(); /** * List of Pixel Android T build id prefixes missing a fix for HDR video with 180 deg rotations * having doubly-rotated thumbnails. * *

    More recent Android T builds should have the fix. */ private static final List PIXEL_T_BUILD_ID_PREFIXES_REQUIRING_HDR_180_ROTATION_FIX = Collections.unmodifiableList(Arrays.asList("TP1A", "TD1A.220804.031")); private static final String WEBM_MIME_TYPE = "video/webm"; private final MediaInitializer initializer; private final BitmapPool bitmapPool; private final MediaMetadataRetrieverFactory factory; @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) public static ResourceDecoder asset(BitmapPool bitmapPool) { return new VideoDecoder<>(bitmapPool, new AssetFileDescriptorInitializer()); } public static ResourceDecoder parcel(BitmapPool bitmapPool) { return new VideoDecoder<>(bitmapPool, new ParcelFileDescriptorInitializer()); } @RequiresApi(api = VERSION_CODES.M) public static ResourceDecoder byteBuffer(BitmapPool bitmapPool) { return new VideoDecoder<>(bitmapPool, new ByteBufferInitializer()); } VideoDecoder(BitmapPool bitmapPool, MediaInitializer initializer) { this(bitmapPool, initializer, DEFAULT_FACTORY); } @VisibleForTesting VideoDecoder( BitmapPool bitmapPool, MediaInitializer initializer, MediaMetadataRetrieverFactory factory) { this.bitmapPool = bitmapPool; this.initializer = initializer; this.factory = factory; } @Override public boolean handles(@NonNull T data, @NonNull Options options) { // Calling setDataSource is expensive so avoid doing so unless we're actually called. // For non-videos this isn't any cheaper, but for videos it saves the redundant call and // 50-100ms. return true; } @Override public Resource decode( @NonNull T resource, int outWidth, int outHeight, @NonNull Options options) throws IOException { long frameTimeMicros = options.get(TARGET_FRAME); if (frameTimeMicros < 0 && frameTimeMicros != DEFAULT_FRAME) { throw new IllegalArgumentException( "Requested frame must be non-negative, or DEFAULT_FRAME, given: " + frameTimeMicros); } Integer frameOption = options.get(FRAME_OPTION); if (frameOption == null) { frameOption = DEFAULT_FRAME_OPTION; } DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION); if (downsampleStrategy == null) { downsampleStrategy = DownsampleStrategy.DEFAULT; } final Bitmap result; MediaMetadataRetriever mediaMetadataRetriever = factory.build(); try { initializer.initializeRetriever(mediaMetadataRetriever, resource); result = decodeFrame( resource, mediaMetadataRetriever, frameTimeMicros, frameOption, outWidth, outHeight, downsampleStrategy); } finally { if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) { mediaMetadataRetriever.close(); } else { mediaMetadataRetriever.release(); } } return BitmapResource.obtain(result, bitmapPool); } @Nullable private Bitmap decodeFrame( @NonNull T resource, MediaMetadataRetriever mediaMetadataRetriever, long frameTimeMicros, int frameOption, int outWidth, int outHeight, DownsampleStrategy strategy) { if (isUnsupportedFormat(resource, mediaMetadataRetriever)) { throw new IllegalStateException("Cannot decode VP8 video on CrOS."); } Bitmap result = null; // Arguably we should handle the case where just width or just height is set to // Target.SIZE_ORIGINAL. Up to and including OMR1, MediaMetadataRetriever defaults to setting // the dimensions to the display width and height if they aren't specified (ie // getScaledFrameAtTime is not used). Given that this is an optimization only if // Target.SIZE_ORIGINAL is not used and not using getScaledFrameAtTime ever would match the // behavior of Glide in all versions of Android prior to OMR1, it's probably fine for now. if (Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1 && outWidth != Target.SIZE_ORIGINAL && outHeight != Target.SIZE_ORIGINAL && strategy != DownsampleStrategy.NONE) { result = decodeScaledFrame( mediaMetadataRetriever, frameTimeMicros, frameOption, outWidth, outHeight, strategy); } if (result == null) { result = decodeOriginalFrame(mediaMetadataRetriever, frameTimeMicros, frameOption); } // MediaMetadataRetriever has a bug where HDR videos with 180 deg rotations are rotated twice, // causing the output frame to appear upside. This needs to be corrected for all versions of // Android until a platform fix lands. result = correctHdr180DegVideoFrameOrientation(mediaMetadataRetriever, result); // Throwing an exception works better in our error logging than returning null. It shouldn't // be expensive because video decoders are attempted after image loads. Video errors are often // logged by the framework, so we can also use this error to suggest callers look for the // appropriate tags in adb. if (result == null) { throw new VideoDecoderException(); } return result; } /** * Corrects the orientation of a bitmap extracted from an HDR video with a 180 degree rotation * angle. * *

    This method will only return a rotated bitmap instead of the input bitmap if * *

      *
    • The Android SDK level is >= R && < T OR the build id is one of T builds without the * platform fix. *
    • The video has a color transfer function with an HLG or ST2084 (PQ) transfer function. *
    • The video has a color standard of BT.2020. *
    • The video has a rotation angle of +/- 180 degrees. *
    */ @TargetApi(Build.VERSION_CODES.R) private static Bitmap correctHdr180DegVideoFrameOrientation( MediaMetadataRetriever mediaMetadataRetriever, Bitmap frame) { if (!isHdr180RotationFixRequired()) { return frame; } boolean requiresHdr180RotationFix = false; try { if (isHDR(mediaMetadataRetriever)) { String rotationString = mediaMetadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); int rotation = Integer.parseInt(rotationString); requiresHdr180RotationFix = Math.abs(rotation) == 180; } } catch (NumberFormatException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Exception trying to extract HDR transfer function or rotation"); } } if (!requiresHdr180RotationFix) { return frame; } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Applying HDR 180 deg thumbnail correction"); } Matrix rotationMatrix = new Matrix(); rotationMatrix.postRotate( /* degrees= */ 180, frame.getWidth() / 2.0f, frame.getHeight() / 2.0f); return Bitmap.createBitmap( frame, /* x= */ 0, /* y= */ 0, frame.getWidth(), frame.getHeight(), rotationMatrix, /* filter= */ true); } @RequiresApi(VERSION_CODES.R) private static boolean isHDR(MediaMetadataRetriever mediaMetadataRetriever) throws NumberFormatException { String colorTransferString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER); String colorStandardString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD); int colorTransfer = Integer.parseInt(colorTransferString); int colorStandard = Integer.parseInt(colorStandardString); // This check needs to match the isHDR check in // frameworks/av/media/libstagefright/FrameDecoder.cpp. return (colorTransfer == MediaFormat.COLOR_TRANSFER_HLG || colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084) && colorStandard == MediaFormat.COLOR_STANDARD_BT2020; } /** Returns true if the build requires a fix for the HDR 180 degree rotation bug. */ @VisibleForTesting static boolean isHdr180RotationFixRequired() { // Only pixel devices have android T builds without the framework fix. if (Build.MODEL.startsWith("Pixel") && VERSION.SDK_INT == VERSION_CODES.TIRAMISU) { return isTBuildRequiringRotationFix(); } else { return VERSION.SDK_INT >= VERSION_CODES.R && VERSION.SDK_INT < VERSION_CODES.TIRAMISU; } } /** * Returns true if the build is an Android T build that requires a fix for the HDR 180 degree * rotation bug. */ private static boolean isTBuildRequiringRotationFix() { for (String buildId : PIXEL_T_BUILD_ID_PREFIXES_REQUIRING_HDR_180_ROTATION_FIX) { if (Build.ID.startsWith(buildId)) { return true; } } return false; } @Nullable @TargetApi(Build.VERSION_CODES.O_MR1) private static Bitmap decodeScaledFrame( MediaMetadataRetriever mediaMetadataRetriever, long frameTimeMicros, int frameOption, int outWidth, int outHeight, DownsampleStrategy strategy) { try { int originalWidth = Integer.parseInt( mediaMetadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); int originalHeight = Integer.parseInt( mediaMetadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); int orientation = Integer.parseInt( mediaMetadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)); if (orientation == 90 || orientation == 270) { int temp = originalWidth; //noinspection SuspiciousNameCombination originalWidth = originalHeight; originalHeight = temp; } float scaleFactor = strategy.getScaleFactor(originalWidth, originalHeight, outWidth, outHeight); int decodeWidth = Math.round(scaleFactor * originalWidth); int decodeHeight = Math.round(scaleFactor * originalHeight); return mediaMetadataRetriever.getScaledFrameAtTime( frameTimeMicros, frameOption, decodeWidth, decodeHeight); } catch (Throwable t) { // This is aggressive, but we'd rather catch errors caused by reading and/or parsing metadata // here and fall back to just decoding the frame whenever possible. If the exception is thrown // just from decoding the frame, then it will be thrown and exposed to callers by the method // below. if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "Exception trying to decode a scaled frame on oreo+, falling back to a fullsize frame", t); } return null; } } private static Bitmap decodeOriginalFrame( MediaMetadataRetriever mediaMetadataRetriever, long frameTimeMicros, int frameOption) { return mediaMetadataRetriever.getFrameAtTime(frameTimeMicros, frameOption); } /** Returns true if the format type is unsupported on the device. */ private boolean isUnsupportedFormat( @NonNull T resource, MediaMetadataRetriever mediaMetadataRetriever) { // MediaFormat.KEY_MIME check below requires at least JELLY_BEAN if (Build.VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN) { return false; } // The primary known problem is vp8 video on ChromeOS (ARC) devices. boolean isArc = Build.DEVICE != null && Build.DEVICE.matches(".+_cheets|cheets_.+"); if (!isArc) { return false; } MediaExtractor mediaExtractor = null; try { // Include the MediaMetadataRetriever extract in the try block out of an abundance of caution. String mimeType = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE); if (!WEBM_MIME_TYPE.equals(mimeType)) { return false; } // Only construct a MediaExtractor for webm files, since the constructor makes a JNI call mediaExtractor = new MediaExtractor(); initializer.initializeExtractor(mediaExtractor, resource); int numTracks = mediaExtractor.getTrackCount(); for (int i = 0; i < numTracks; ++i) { MediaFormat mediaformat = mediaExtractor.getTrackFormat(i); String trackMimeType = mediaformat.getString(MediaFormat.KEY_MIME); if (MediaFormat.MIMETYPE_VIDEO_VP8.equals(trackMimeType)) { return true; } } } catch (Throwable t) { // Catching everything here out of an abundance of caution if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Exception trying to extract track info for a webm video on CrOS.", t); } } finally { if (mediaExtractor != null) { mediaExtractor.release(); } } return false; } @VisibleForTesting static class MediaMetadataRetrieverFactory { public MediaMetadataRetriever build() { return new MediaMetadataRetriever(); } } @VisibleForTesting interface MediaInitializer { void initializeRetriever(MediaMetadataRetriever retriever, T data); @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) void initializeExtractor(MediaExtractor extractor, T data) throws IOException; } @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) private static final class AssetFileDescriptorInitializer implements MediaInitializer { @Override public void initializeRetriever(MediaMetadataRetriever retriever, AssetFileDescriptor data) { retriever.setDataSource(data.getFileDescriptor(), data.getStartOffset(), data.getLength()); } @Override public void initializeExtractor(MediaExtractor extractor, AssetFileDescriptor data) throws IOException { extractor.setDataSource(data.getFileDescriptor(), data.getStartOffset(), data.getLength()); } } // Visible for VideoBitmapDecoder. static final class ParcelFileDescriptorInitializer implements MediaInitializer { @Override public void initializeRetriever(MediaMetadataRetriever retriever, ParcelFileDescriptor data) { retriever.setDataSource(data.getFileDescriptor()); } @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void initializeExtractor(MediaExtractor extractor, ParcelFileDescriptor data) throws IOException { extractor.setDataSource(data.getFileDescriptor()); } } @RequiresApi(Build.VERSION_CODES.M) static final class ByteBufferInitializer implements MediaInitializer { @Override public void initializeRetriever(MediaMetadataRetriever retriever, final ByteBuffer data) { retriever.setDataSource(getMediaDataSource(data)); } @Override public void initializeExtractor(MediaExtractor extractor, final ByteBuffer data) throws IOException { extractor.setDataSource(getMediaDataSource(data)); } private MediaDataSource getMediaDataSource(final ByteBuffer data) { return new MediaDataSource() { @Override public int readAt(long position, byte[] buffer, int offset, int size) { if (position >= data.limit()) { return -1; } data.position((int) position); int numBytesRead = Math.min(size, data.remaining()); data.get(buffer, offset, numBytesRead); return numBytesRead; } @Override public long getSize() { return data.limit(); } @Override public void close() {} }; } } private static final class VideoDecoderException extends RuntimeException { private static final long serialVersionUID = -2556382523004027815L; VideoDecoderException() { super( "MediaMetadataRetriever failed to retrieve a frame without throwing, check the adb logs" + " for .*MetadataRetriever.* prior to this exception for details"); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bytes/ByteBufferRewinder.java ================================================ package com.bumptech.glide.load.resource.bytes; import androidx.annotation.NonNull; import com.bumptech.glide.load.data.DataRewinder; import java.nio.ByteBuffer; /** Rewinds {@link java.nio.ByteBuffer}s. */ public class ByteBufferRewinder implements DataRewinder { private final ByteBuffer buffer; // Public API. @SuppressWarnings("WeakerAccess") public ByteBufferRewinder(ByteBuffer buffer) { this.buffer = buffer; } @NonNull @Override public ByteBuffer rewindAndGet() { buffer.position(0); return buffer; } @Override public void cleanup() { // Do nothing. } /** Factory for {@link com.bumptech.glide.load.resource.bytes.ByteBufferRewinder}. */ public static class Factory implements DataRewinder.Factory { @NonNull @Override public DataRewinder build(ByteBuffer data) { return new ByteBufferRewinder(data); } @NonNull @Override public Class getDataClass() { return ByteBuffer.class; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/bytes/BytesResource.java ================================================ package com.bumptech.glide.load.resource.bytes; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.Preconditions; /** An {@link com.bumptech.glide.load.engine.Resource} wrapping a byte array. */ public class BytesResource implements Resource { private final byte[] bytes; public BytesResource(byte[] bytes) { this.bytes = Preconditions.checkNotNull(bytes); } @NonNull @Override public Class getResourceClass() { return byte[].class; } /** * In most cases it will only be retrieved once (see linked methods). * * @return the same array every time, do not mutate the contents. Not a copy returned, because * copying the array can be prohibitively expensive and/or lead to OOMs. * @see com.bumptech.glide.load.ResourceEncoder * @see com.bumptech.glide.load.resource.transcode.ResourceTranscoder * @see com.bumptech.glide.request.SingleRequest#onResourceReady */ @NonNull @Override @SuppressWarnings("PMD.MethodReturnsInternalArray") public byte[] get() { return bytes; } @Override public int getSize() { return bytes.length; } @Override public void recycle() { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/AnimatedImageDecoder.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.graphics.Bitmap; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.Source; import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.resource.DefaultOnHeaderDecodedListener; import com.bumptech.glide.util.ByteBufferUtil; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.List; /** * Allows decoding animated images using {@link ImageDecoder}. * *

    Supported formats: WebP on Android P+. AVIF on Android 12/S+. */ @RequiresApi(Build.VERSION_CODES.P) public final class AnimatedImageDecoder { private final List imageHeaderParsers; private final ArrayPool arrayPool; public static ResourceDecoder streamDecoder( List imageHeaderParsers, ArrayPool arrayPool) { return new StreamAnimatedImageDecoder(new AnimatedImageDecoder(imageHeaderParsers, arrayPool)); } public static ResourceDecoder byteBufferDecoder( List imageHeaderParsers, ArrayPool arrayPool) { return new ByteBufferAnimatedImageDecoder( new AnimatedImageDecoder(imageHeaderParsers, arrayPool)); } private AnimatedImageDecoder(List imageHeaderParsers, ArrayPool arrayPool) { this.imageHeaderParsers = imageHeaderParsers; this.arrayPool = arrayPool; } @Synthetic boolean handles(ByteBuffer byteBuffer) throws IOException { return isHandled(ImageHeaderParserUtils.getType(imageHeaderParsers, byteBuffer)); } @Synthetic boolean handles(InputStream is) throws IOException { return isHandled(ImageHeaderParserUtils.getType(imageHeaderParsers, is, arrayPool)); } @SuppressWarnings("checkstyle:UnnecessaryParentheses") // Readability private boolean isHandled(ImageType imageType) { return imageType == ImageType.ANIMATED_WEBP || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && imageType == ImageType.ANIMATED_AVIF); } @Synthetic Resource decode(@NonNull Source source, int width, int height, @NonNull Options options) throws IOException { Drawable decoded = ImageDecoder.decodeDrawable( source, new DefaultOnHeaderDecodedListener(width, height, options)); if (!(decoded instanceof AnimatedImageDrawable)) { throw new IOException( "Received unexpected drawable type for animated image, failing: " + decoded); } return new AnimatedImageDrawableResource((AnimatedImageDrawable) decoded); } private static final class AnimatedImageDrawableResource implements Resource { /** A totally made up number of the number of frames we think are held in memory at once... */ private static final int ESTIMATED_NUMBER_OF_FRAMES = 2; private final AnimatedImageDrawable imageDrawable; AnimatedImageDrawableResource(AnimatedImageDrawable imageDrawable) { this.imageDrawable = imageDrawable; } @NonNull @Override public Class getResourceClass() { return Drawable.class; } @NonNull @Override public AnimatedImageDrawable get() { return imageDrawable; } @Override public int getSize() { return imageDrawable.getIntrinsicWidth() * imageDrawable.getIntrinsicHeight() * Util.getBytesPerPixel(Bitmap.Config.ARGB_8888) * ESTIMATED_NUMBER_OF_FRAMES; } @Override public void recycle() { imageDrawable.stop(); imageDrawable.clearAnimationCallbacks(); } } private static final class StreamAnimatedImageDecoder implements ResourceDecoder { private final AnimatedImageDecoder delegate; StreamAnimatedImageDecoder(AnimatedImageDecoder delegate) { this.delegate = delegate; } @Override public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException { return delegate.handles(source); } @Override public Resource decode( @NonNull InputStream is, int width, int height, @NonNull Options options) throws IOException { Source source = ImageDecoder.createSource(ByteBufferUtil.fromStream(is)); return delegate.decode(source, width, height, options); } } private static final class ByteBufferAnimatedImageDecoder implements ResourceDecoder { private final AnimatedImageDecoder delegate; ByteBufferAnimatedImageDecoder(AnimatedImageDecoder delegate) { this.delegate = delegate; } @Override public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException { return delegate.handles(source); } @Override public Resource decode( @NonNull ByteBuffer byteBuffer, int width, int height, @NonNull Options options) throws IOException { Source source = ImageDecoder.createSource(byteBuffer); return delegate.decode(source, width, height, options); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/AnimatedWebpDecoder.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.graphics.Bitmap; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.Source; import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.resource.DefaultOnHeaderDecodedListener; import com.bumptech.glide.util.ByteBufferUtil; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.List; /** * Allows decoding animated webp images using {@link ImageDecoder} on Android P+. @Deprecated This * class has been replaced by {@link AnimatedImageDecoder} and is not used in Glide by default. It * will be removed in a future version. */ @Deprecated @RequiresApi(Build.VERSION_CODES.P) public final class AnimatedWebpDecoder { private final List imageHeaderParsers; private final ArrayPool arrayPool; public static ResourceDecoder streamDecoder( List imageHeaderParsers, ArrayPool arrayPool) { return new StreamAnimatedWebpDecoder(new AnimatedWebpDecoder(imageHeaderParsers, arrayPool)); } public static ResourceDecoder byteBufferDecoder( List imageHeaderParsers, ArrayPool arrayPool) { return new ByteBufferAnimatedWebpDecoder( new AnimatedWebpDecoder(imageHeaderParsers, arrayPool)); } private AnimatedWebpDecoder(List imageHeaderParsers, ArrayPool arrayPool) { this.imageHeaderParsers = imageHeaderParsers; this.arrayPool = arrayPool; } @Synthetic boolean handles(ByteBuffer byteBuffer) throws IOException { return isHandled(ImageHeaderParserUtils.getType(imageHeaderParsers, byteBuffer)); } @Synthetic boolean handles(InputStream is) throws IOException { return isHandled(ImageHeaderParserUtils.getType(imageHeaderParsers, is, arrayPool)); } private boolean isHandled(ImageType imageType) { return imageType == ImageType.ANIMATED_WEBP; } @Synthetic Resource decode(@NonNull Source source, int width, int height, @NonNull Options options) throws IOException { Drawable decoded = ImageDecoder.decodeDrawable( source, new DefaultOnHeaderDecodedListener(width, height, options)); if (!(decoded instanceof AnimatedImageDrawable)) { throw new IOException( "Received unexpected drawable type for animated webp, failing: " + decoded); } return new AnimatedImageDrawableResource((AnimatedImageDrawable) decoded); } private static final class AnimatedImageDrawableResource implements Resource { /** A totally made up number of the number of frames we think are held in memory at once... */ private static final int ESTIMATED_NUMBER_OF_FRAMES = 2; private final AnimatedImageDrawable imageDrawable; AnimatedImageDrawableResource(AnimatedImageDrawable imageDrawable) { this.imageDrawable = imageDrawable; } @NonNull @Override public Class getResourceClass() { return Drawable.class; } @NonNull @Override public AnimatedImageDrawable get() { return imageDrawable; } @Override public int getSize() { return imageDrawable.getIntrinsicWidth() * imageDrawable.getIntrinsicHeight() * Util.getBytesPerPixel(Bitmap.Config.ARGB_8888) * ESTIMATED_NUMBER_OF_FRAMES; } @Override public void recycle() { imageDrawable.stop(); imageDrawable.clearAnimationCallbacks(); } } private static final class StreamAnimatedWebpDecoder implements ResourceDecoder { private final AnimatedWebpDecoder delegate; StreamAnimatedWebpDecoder(AnimatedWebpDecoder delegate) { this.delegate = delegate; } @Override public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException { return delegate.handles(source); } @Override public Resource decode( @NonNull InputStream is, int width, int height, @NonNull Options options) throws IOException { Source source = ImageDecoder.createSource(ByteBufferUtil.fromStream(is)); return delegate.decode(source, width, height, options); } } private static final class ByteBufferAnimatedWebpDecoder implements ResourceDecoder { private final AnimatedWebpDecoder delegate; ByteBufferAnimatedWebpDecoder(AnimatedWebpDecoder delegate) { this.delegate = delegate; } @Override public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException { return delegate.handles(source); } @Override public Resource decode( @NonNull ByteBuffer byteBuffer, int width, int height, @NonNull Options options) throws IOException { Source source = ImageDecoder.createSource(byteBuffer); return delegate.decode(source, width, height, options); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/DrawableDecoderCompat.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.content.Context; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.ContextThemeWrapper; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; /** * Handles decoding Drawables with the v7 support library if present and falling back to the v4 * support library otherwise. */ public final class DrawableDecoderCompat { private static volatile boolean shouldCallAppCompatResources = true; private DrawableDecoderCompat() { // Utility class. } /** See {@code getDrawable(Context, int, Theme)}. */ public static Drawable getDrawable( Context ourContext, Context targetContext, @DrawableRes int id) { return getDrawable(ourContext, targetContext, id, /* theme= */ null); } /** * Loads a Drawable using {@link AppCompatResources} if available and {@link ResourcesCompat} * otherwise, depending on whether or not the v7 support library is included in the application. * * @param theme Used instead of the {@link Theme} returned from the given {@link Context} if * non-null when loading the {@link Drawable}. */ public static Drawable getDrawable( Context ourContext, @DrawableRes int id, @Nullable Theme theme) { return getDrawable(ourContext, ourContext, id, theme); } private static Drawable getDrawable( Context ourContext, Context targetContext, @DrawableRes int id, @Nullable Theme theme) { try { // Race conditions may cause us to attempt to load using v7 more than once. That's ok since // this check is a modest optimization and the output will be correct anyway. if (shouldCallAppCompatResources) { return loadDrawableV7(targetContext, id, theme); } } catch (NoClassDefFoundError error) { shouldCallAppCompatResources = false; } catch (IllegalStateException e) { if (ourContext.getPackageName().equals(targetContext.getPackageName())) { throw e; } return ContextCompat.getDrawable(targetContext, id); } catch (Resources.NotFoundException e) { // Ignored, this can be thrown when drawable compat attempts to decode a canary resource. If // that decode attempt fails, we still want to try with the v4 ResourcesCompat below. } return loadDrawableV4(targetContext, id, theme != null ? theme : targetContext.getTheme()); } private static Drawable loadDrawableV7( Context context, @DrawableRes int id, @Nullable Theme theme) { Context resourceContext; if (theme != null && VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(context, theme); contextThemeWrapper.applyOverrideConfiguration(theme.getResources().getConfiguration()); resourceContext = contextThemeWrapper; } else { resourceContext = context; } return AppCompatResources.getDrawable(resourceContext, id); } private static Drawable loadDrawableV4( Context context, @DrawableRes int id, @Nullable Theme theme) { return ResourcesCompat.getDrawable(context.getResources(), id, theme); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/DrawableResource.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable.ConstantState; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.engine.Initializable; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.util.Preconditions; /** * Simple wrapper for an Android {@link Drawable} which returns a {@link * android.graphics.drawable.Drawable.ConstantState#newDrawable() new drawable} based on it's {@link * android.graphics.drawable.Drawable.ConstantState state}. * *

    Suggested usages only include {@code T}s where the new drawable is of the same or * descendant class. * * @param type of the wrapped {@link Drawable} */ public abstract class DrawableResource implements Resource, Initializable { protected final T drawable; public DrawableResource(T drawable) { this.drawable = Preconditions.checkNotNull(drawable); } @NonNull @SuppressWarnings("unchecked") @Override public final T get() { @Nullable ConstantState state = drawable.getConstantState(); if (state == null) { return drawable; } // Drawables contain temporary state related to how they're being displayed // (alpha, color filter etc), so return a new copy each time. // If we ever return the original drawable, it's temporary state may be changed // and subsequent copies may end up with that temporary state. See #276. return (T) state.newDrawable(); } @Override public void initialize() { if (drawable instanceof BitmapDrawable) { ((BitmapDrawable) drawable).getBitmap().prepareToDraw(); } else if (drawable instanceof GifDrawable) { ((GifDrawable) drawable).getFirstFrame().prepareToDraw(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/DrawableTransitionOptions.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.TransitionOptions; import com.bumptech.glide.request.transition.DrawableCrossFadeFactory; import com.bumptech.glide.request.transition.TransitionFactory; /** Contains {@link Drawable} specific animation options. */ // Public API. @SuppressWarnings("WeakerAccess") public final class DrawableTransitionOptions extends TransitionOptions { /** * Returns a {@link DrawableTransitionOptions} object that enables a cross fade animation. * * @see #crossFade() */ @NonNull public static DrawableTransitionOptions withCrossFade() { return new DrawableTransitionOptions().crossFade(); } /** * Returns a {@link DrawableTransitionOptions} object that enables a cross fade animation. * * @see #crossFade(int) */ @NonNull public static DrawableTransitionOptions withCrossFade(int duration) { return new DrawableTransitionOptions().crossFade(duration); } /** * Returns a {@link DrawableTransitionOptions} object that enables a cross fade animation. * * @see #crossFade(DrawableCrossFadeFactory) */ @NonNull public static DrawableTransitionOptions withCrossFade( @NonNull DrawableCrossFadeFactory drawableCrossFadeFactory) { return new DrawableTransitionOptions().crossFade(drawableCrossFadeFactory); } /** * Returns a {@link DrawableTransitionOptions} object that enables a cross fade animation. * * @see #crossFade(DrawableCrossFadeFactory.Builder) */ @NonNull public static DrawableTransitionOptions withCrossFade( @NonNull DrawableCrossFadeFactory.Builder builder) { return new DrawableTransitionOptions().crossFade(builder); } /** * Returns a {@link DrawableTransitionOptions} object that uses the given transition factory. * * @see com.bumptech.glide.GenericTransitionOptions#with(TransitionFactory) */ @NonNull public static DrawableTransitionOptions with( @NonNull TransitionFactory transitionFactory) { return new DrawableTransitionOptions().transition(transitionFactory); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). */ @NonNull public DrawableTransitionOptions crossFade() { return crossFade(new DrawableCrossFadeFactory.Builder()); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). * * @param duration The duration of the animation, see {@code * DrawableCrossFadeFactory.Builder(int)} * @see com.bumptech.glide.request.transition.DrawableCrossFadeFactory.Builder */ @NonNull public DrawableTransitionOptions crossFade(int duration) { return crossFade(new DrawableCrossFadeFactory.Builder(duration)); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). */ @NonNull public DrawableTransitionOptions crossFade( @NonNull DrawableCrossFadeFactory drawableCrossFadeFactory) { return transition(drawableCrossFadeFactory); } /** * Enables a cross fade animation between both the placeholder and the first resource and between * subsequent resources (if thumbnails are used). */ @NonNull public DrawableTransitionOptions crossFade(@NonNull DrawableCrossFadeFactory.Builder builder) { return crossFade(builder.build()); } // Make sure that we're not equal to any other concrete implementation of TransitionOptions. @Override public boolean equals(Object o) { return o instanceof DrawableTransitionOptions && super.equals(o); } // Our class doesn't include any additional properties, so we don't need to modify hashcode, but // keep it here as a reminder in case we add properties. @SuppressWarnings("PMD.UselessOverridingMethod") @Override public int hashCode() { return super.hashCode(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/NonOwnedDrawableResource.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.engine.Resource; /** * Handles generic {@link Drawable} types where we may be uncertain of their size or type and where * we don't know that it's safe for us to recycle or re-use the Drawable. */ final class NonOwnedDrawableResource extends DrawableResource { @SuppressWarnings("unchecked") @Nullable static Resource newInstance(@Nullable Drawable drawable) { return drawable != null ? new NonOwnedDrawableResource(drawable) : null; } private NonOwnedDrawableResource(Drawable drawable) { super(drawable); } @NonNull @SuppressWarnings("unchecked") @Override public Class getResourceClass() { return (Class) drawable.getClass(); } @Override public int getSize() { // 4 bytes per pixel for ARGB_8888 Bitmaps is something of a reasonable approximation. If // there are no intrinsic bounds, we can fall back just to 1. return Math.max(1, drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 4); } @Override public void recycle() { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/ResourceDrawableDecoder.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.TextUtils; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.Preconditions; import java.util.List; /** * Decodes {@link Drawable}s given resource {@link Uri}s. * *

    This is typically used as a fallback for resource types that either aren't Bitmaps (see #350) * or for resource types that we can't obtain an {@link java.io.InputStream} for using a standard * {@link ContentResolver}, including some types of application icons and resources loaded from * other packages. */ public class ResourceDrawableDecoder implements ResourceDecoder { /** Specifies a {@link Theme} which will be used to load the drawable. */ public static final Option THEME = Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.Theme"); /** * The package name to provide {@link Resources#getIdentifier(String, String, String)} when trying * to find system resource ids. * *

    As far as I can tell this is undocumented, but works. */ private static final String ANDROID_PACKAGE_NAME = "android"; /** * {@link Resources#getIdentifier(String, String, String)} documents that it will return 0 and * that 0 is not a valid resouce id. */ private static final int MISSING_RESOURCE_ID = 0; // android.resource:////. private static final int NAME_URI_PATH_SEGMENTS = 2; private static final int TYPE_PATH_SEGMENT_INDEX = 0; private static final int NAME_PATH_SEGMENT_INDEX = 1; // android.resource:/// private static final int ID_PATH_SEGMENTS = 1; private static final int RESOURCE_ID_SEGMENT_INDEX = 0; private final Context context; public ResourceDrawableDecoder(Context context) { this.context = context.getApplicationContext(); } @Override public boolean handles(@NonNull Uri source, @NonNull Options options) { String scheme = source.getScheme(); return scheme != null && scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE); } @Nullable @Override public Resource decode( @NonNull Uri source, int width, int height, @NonNull Options options) { String packageName = source.getAuthority(); if (TextUtils.isEmpty(packageName)) { throw new IllegalStateException("Package name for " + source + " is null or empty"); } Context targetContext = findContextForPackage(source, packageName); @DrawableRes int resId = findResourceIdFromUri(targetContext, source); // Only use the provided theme if we're loading resources from our package. We can't get themes // from other packages and we don't want to use a theme from our package when loading another // package's resources. Theme theme = Preconditions.checkNotNull(packageName).equals(context.getPackageName()) ? options.get(THEME) : null; Drawable drawable = theme == null ? DrawableDecoderCompat.getDrawable(context, targetContext, resId) : DrawableDecoderCompat.getDrawable(context, resId, theme); return NonOwnedDrawableResource.newInstance(drawable); } @NonNull private Context findContextForPackage(Uri source, @NonNull String packageName) { // Fast path if (packageName.equals(context.getPackageName())) { return context; } try { return context.createPackageContext(packageName, /* flags= */ 0); } catch (NameNotFoundException e) { // The parent APK holds the correct context if the resource is located in a split if (packageName.contains(context.getPackageName())) { return context; } throw new IllegalArgumentException( "Failed to obtain context or unrecognized Uri format for: " + source, e); } } @DrawableRes private int findResourceIdFromUri(Context context, Uri source) { List segments = source.getPathSegments(); if (segments.size() == NAME_URI_PATH_SEGMENTS) { return findResourceIdFromTypeAndNameResourceUri(context, source); } else if (segments.size() == ID_PATH_SEGMENTS) { return findResourceIdFromResourceIdUri(source); } else { throw new IllegalArgumentException("Unrecognized Uri format: " + source); } } // android.resource://com.android.camera2/mipmap/logo_camera_color @DrawableRes private int findResourceIdFromTypeAndNameResourceUri(Context context, Uri source) { List segments = source.getPathSegments(); String packageName = source.getAuthority(); String typeName = segments.get(TYPE_PATH_SEGMENT_INDEX); String resourceName = segments.get(NAME_PATH_SEGMENT_INDEX); int result = context.getResources().getIdentifier(resourceName, typeName, packageName); if (result == MISSING_RESOURCE_ID) { result = Resources.getSystem().getIdentifier(resourceName, typeName, ANDROID_PACKAGE_NAME); } if (result == MISSING_RESOURCE_ID) { throw new IllegalArgumentException("Failed to find resource id for: " + source); } return result; } // android.resource://com.android.camera2/123456 @DrawableRes private int findResourceIdFromResourceIdUri(Uri source) { List segments = source.getPathSegments(); try { return Integer.parseInt(segments.get(RESOURCE_ID_SEGMENT_INDEX)); } catch (NumberFormatException e) { throw new IllegalArgumentException("Unrecognized Uri format: " + source, e); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/drawable/UnitDrawableDecoder.java ================================================ package com.bumptech.glide.load.resource.drawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; /** Passes through a {@link Drawable} as a {@link Drawable} based {@link Resource}. */ public class UnitDrawableDecoder implements ResourceDecoder { @Override public boolean handles(@NonNull Drawable source, @NonNull Options options) { return true; } @Nullable @Override public Resource decode( @NonNull Drawable source, int width, int height, @NonNull Options options) { return NonOwnedDrawableResource.newInstance(source); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/file/FileDecoder.java ================================================ package com.bumptech.glide.load.resource.file; import androidx.annotation.NonNull; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import java.io.File; /** * A simple {@link com.bumptech.glide.load.ResourceDecoder} that creates resource for a given {@link * java.io.File}. */ public class FileDecoder implements ResourceDecoder { @Override public boolean handles(@NonNull File source, @NonNull Options options) { return true; } @Override public Resource decode( @NonNull File source, int width, int height, @NonNull Options options) { return new FileResource(source); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/file/FileResource.java ================================================ package com.bumptech.glide.load.resource.file; import com.bumptech.glide.load.resource.SimpleResource; import java.io.File; /** A simple {@link com.bumptech.glide.load.engine.Resource} that wraps a {@link File}. */ // Public API. @SuppressWarnings("WeakerAccess") public class FileResource extends SimpleResource { public FileResource(File file) { super(file); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/ByteBufferGifDecoder.java ================================================ package com.bumptech.glide.load.resource.gif; import android.content.Context; import android.graphics.Bitmap; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.Glide; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.gifdecoder.GifHeader; import com.bumptech.glide.gifdecoder.GifHeaderParser; import com.bumptech.glide.gifdecoder.StandardGifDecoder; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.UnitTransformation; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Util; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; import java.util.Queue; /** * An {@link com.bumptech.glide.load.ResourceDecoder} that decodes {@link * com.bumptech.glide.load.resource.gif.GifDrawable} from {@link java.io.InputStream} data. */ public class ByteBufferGifDecoder implements ResourceDecoder { private static final String TAG = "BufferGifDecoder"; private static final GifDecoderFactory GIF_DECODER_FACTORY = new GifDecoderFactory(); private static final GifHeaderParserPool PARSER_POOL = new GifHeaderParserPool(); private final Context context; private final List parsers; private final GifHeaderParserPool parserPool; private final GifDecoderFactory gifDecoderFactory; private final GifBitmapProvider provider; // Public API. @SuppressWarnings("unused") public ByteBufferGifDecoder(Context context) { this( context, Glide.get(context).getRegistry().getImageHeaderParsers(), Glide.get(context).getBitmapPool(), Glide.get(context).getArrayPool()); } public ByteBufferGifDecoder( Context context, List parsers, BitmapPool bitmapPool, ArrayPool arrayPool) { this(context, parsers, bitmapPool, arrayPool, PARSER_POOL, GIF_DECODER_FACTORY); } @VisibleForTesting ByteBufferGifDecoder( Context context, List parsers, BitmapPool bitmapPool, ArrayPool arrayPool, GifHeaderParserPool parserPool, GifDecoderFactory gifDecoderFactory) { this.context = context.getApplicationContext(); this.parsers = parsers; this.gifDecoderFactory = gifDecoderFactory; this.provider = new GifBitmapProvider(bitmapPool, arrayPool); this.parserPool = parserPool; } @Override public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException { return !options.get(GifOptions.DISABLE_ANIMATION) && ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF; } @Override public GifDrawableResource decode( @NonNull ByteBuffer source, int width, int height, @NonNull Options options) { final GifHeaderParser parser = parserPool.obtain(source); try { return decode(source, width, height, parser, options); } finally { parserPool.release(parser); } } @Nullable private GifDrawableResource decode( ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) { long startTime = LogTime.getLogTime(); try { final GifHeader header = parser.parseHeader(); if (header.getNumFrames() <= 0 || header.getStatus() != GifDecoder.STATUS_OK) { // If we couldn't decode the GIF, we will end up with a frame count of 0. return null; } Bitmap.Config config = options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565 ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888; int sampleSize = getSampleSize(header, width, height); GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize); gifDecoder.setDefaultBitmapConfig(config); gifDecoder.advance(); Bitmap firstFrame = gifDecoder.getNextFrame(); if (firstFrame == null) { return null; } Transformation unitTransformation = UnitTransformation.get(); GifDrawable gifDrawable = new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame); return new GifDrawableResource(gifDrawable); } finally { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime)); } } } private static int getSampleSize(GifHeader gifHeader, int targetWidth, int targetHeight) { int exactSampleSize = Math.min(gifHeader.getHeight() / targetHeight, gifHeader.getWidth() / targetWidth); int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize); // Although functionally equivalent to 0 for BitmapFactory, 1 is a safer default for our code // than 0. int sampleSize = Math.max(1, powerOfTwoSampleSize); if (Log.isLoggable(TAG, Log.VERBOSE) && sampleSize > 1) { Log.v( TAG, "Downsampling GIF" + ", sampleSize: " + sampleSize + ", target dimens: [" + targetWidth + "x" + targetHeight + "]" + ", actual dimens: [" + gifHeader.getWidth() + "x" + gifHeader.getHeight() + "]"); } return sampleSize; } @VisibleForTesting static class GifDecoderFactory { GifDecoder build( GifDecoder.BitmapProvider provider, GifHeader header, ByteBuffer data, int sampleSize) { return new StandardGifDecoder(provider, header, data, sampleSize); } } @VisibleForTesting static class GifHeaderParserPool { private final Queue pool = Util.createQueue(0); synchronized GifHeaderParser obtain(ByteBuffer buffer) { GifHeaderParser result = pool.poll(); if (result == null) { result = new GifHeaderParser(); } return result.setData(buffer); } synchronized void release(GifHeaderParser parser) { parser.clear(); pool.offer(parser); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifBitmapProvider.java ================================================ package com.bumptech.glide.load.resource.gif; import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; /** * Implements {@link com.bumptech.glide.gifdecoder.GifDecoder.BitmapProvider} by wrapping Glide's * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool}. */ public final class GifBitmapProvider implements GifDecoder.BitmapProvider { private final BitmapPool bitmapPool; @Nullable private final ArrayPool arrayPool; /** * Constructs an instance without a shared byte array pool. Byte arrays will be always constructed * when requested. */ public GifBitmapProvider(BitmapPool bitmapPool) { this(bitmapPool, /* arrayPool= */ null); } /** Constructs an instance with a shared array pool. Arrays will be reused where possible. */ // Public API. @SuppressWarnings("WeakerAccess") public GifBitmapProvider(BitmapPool bitmapPool, @Nullable ArrayPool arrayPool) { this.bitmapPool = bitmapPool; this.arrayPool = arrayPool; } @NonNull @Override public Bitmap obtain(int width, int height, @NonNull Bitmap.Config config) { return bitmapPool.getDirty(width, height, config); } @Override public void release(@NonNull Bitmap bitmap) { bitmapPool.put(bitmap); } @NonNull @Override public byte[] obtainByteArray(int size) { if (arrayPool == null) { return new byte[size]; } return arrayPool.get(size, byte[].class); } @Override public void release(@NonNull byte[] bytes) { if (arrayPool == null) { return; } arrayPool.put(bytes); } @NonNull @Override public int[] obtainIntArray(int size) { if (arrayPool == null) { return new int[size]; } return arrayPool.get(size, int[].class); } @SuppressWarnings("PMD.UseVarargs") @Override public void release(@NonNull int[] array) { if (arrayPool == null) { return; } arrayPool.put(array); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawable.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.gifdecoder.GifDecoder.TOTAL_ITERATION_COUNT_FOREVER; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.view.Gravity; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.vectordrawable.graphics.drawable.Animatable2Compat; import com.bumptech.glide.Glide; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; /** * An animated {@link android.graphics.drawable.Drawable} that plays the frames of an animated GIF. */ public class GifDrawable extends Drawable implements GifFrameLoader.FrameCallback, Animatable, Animatable2Compat { /** A constant indicating that an animated drawable should loop continuously. */ // Public API. @SuppressWarnings("WeakerAccess") public static final int LOOP_FOREVER = -1; /** * A constant indicating that an animated drawable should loop for its default number of times. * For animated GIFs, this constant indicates the GIF should use the netscape loop count if * present. */ // Public API. @SuppressWarnings("WeakerAccess") public static final int LOOP_INTRINSIC = 0; private static final int GRAVITY = Gravity.FILL; private final GifState state; /** True if the drawable is currently animating. */ private boolean isRunning; /** True if the drawable should animate while visible. */ private boolean isStarted; /** True if the drawable's resources have been recycled. */ private boolean isRecycled; /** * True if the drawable is currently visible. Default to true because on certain platforms (at * least 4.1.1), setVisible is not called on {@link android.graphics.drawable.Drawable Drawables} * during {@link android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. * See issue #130. */ private boolean isVisible = true; /** The number of times we've looped over all the frames in the GIF. */ private int loopCount; /** The number of times to loop through the GIF animation. */ private int maxLoopCount = LOOP_FOREVER; private boolean applyGravity; private Paint paint; private Rect destRect; /** Callbacks to notify loop completion of a gif, where the loop count is explicitly specified. */ private List animationCallbacks; /** * Constructor for GifDrawable. * * @param context A context. * @param bitmapPool Ignored, see deprecation note. * @param frameTransformation An {@link com.bumptech.glide.load.Transformation} that can be * applied to each frame. * @param targetFrameWidth The desired width of the frames displayed by this drawable (the width * of the view or {@link com.bumptech.glide.request.target.Target} this drawable is being * loaded into). * @param targetFrameHeight The desired height of the frames displayed by this drawable (the * height of the view or {@link com.bumptech.glide.request.target.Target} this drawable is * being loaded into). * @param gifDecoder The decoder to use to decode GIF data. * @param firstFrame The decoded and transformed first frame of this GIF. * @see #setFrameTransformation(com.bumptech.glide.load.Transformation, android.graphics.Bitmap) * @deprecated Use {@link #GifDrawable(Context, GifDecoder, Transformation, int, int, Bitmap)} */ @SuppressWarnings("deprecation") @Deprecated public GifDrawable( Context context, GifDecoder gifDecoder, @SuppressWarnings("unused") BitmapPool bitmapPool, Transformation frameTransformation, int targetFrameWidth, int targetFrameHeight, Bitmap firstFrame) { this(context, gifDecoder, frameTransformation, targetFrameWidth, targetFrameHeight, firstFrame); } /** * Constructor for GifDrawable. * * @param context A context. * @param frameTransformation An {@link com.bumptech.glide.load.Transformation} that can be * applied to each frame. * @param targetFrameWidth The desired width of the frames displayed by this drawable (the width * of the view or {@link com.bumptech.glide.request.target.Target} this drawable is being * loaded into). * @param targetFrameHeight The desired height of the frames displayed by this drawable (the * height of the view or {@link com.bumptech.glide.request.target.Target} this drawable is * being loaded into). * @param gifDecoder The decoder to use to decode GIF data. * @param firstFrame The decoded and transformed first frame of this GIF. * @see #setFrameTransformation(com.bumptech.glide.load.Transformation, android.graphics.Bitmap) */ public GifDrawable( Context context, GifDecoder gifDecoder, Transformation frameTransformation, int targetFrameWidth, int targetFrameHeight, Bitmap firstFrame) { this( new GifState( new GifFrameLoader( // TODO(b/27524013): Factor out this call to Glide.get() Glide.get(context), gifDecoder, targetFrameWidth, targetFrameHeight, frameTransformation, firstFrame))); } GifDrawable(GifState state) { this.state = Preconditions.checkNotNull(state); } @VisibleForTesting GifDrawable(GifFrameLoader frameLoader, Paint paint) { this(new GifState(frameLoader)); this.paint = paint; } public int getSize() { return state.frameLoader.getSize(); } public Bitmap getFirstFrame() { return state.frameLoader.getFirstFrame(); } // Public API. @SuppressWarnings("WeakerAccess") public void setFrameTransformation( Transformation frameTransformation, Bitmap firstFrame) { state.frameLoader.setFrameTransformation(frameTransformation, firstFrame); } public Transformation getFrameTransformation() { return state.frameLoader.getFrameTransformation(); } public ByteBuffer getBuffer() { return state.frameLoader.getBuffer(); } public int getFrameCount() { return state.frameLoader.getFrameCount(); } /** * Returns the current frame index in the range 0..{@link #getFrameCount()} - 1, or -1 if no frame * is displayed. */ // Public API. @SuppressWarnings("WeakerAccess") public int getFrameIndex() { return state.frameLoader.getCurrentIndex(); } private void resetLoopCount() { loopCount = 0; } /** * Starts the animation from the first frame. Can only be called while animation is not running. */ // Public API. @SuppressWarnings("unused") public void startFromFirstFrame() { Preconditions.checkArgument(!isRunning, "You cannot restart a currently running animation."); state.frameLoader.setNextStartFromFirstFrame(); start(); } @Override public void start() { isStarted = true; resetLoopCount(); if (isVisible) { startRunning(); } } @Override public void stop() { isStarted = false; stopRunning(); } private void startRunning() { Preconditions.checkArgument( !isRecycled, "You cannot start a recycled Drawable. Ensure that" + "you clear any references to the Drawable when clearing the corresponding request."); // If we have only a single frame, we don't want to decode it endlessly. if (state.frameLoader.getFrameCount() == 1) { invalidateSelf(); } else if (!isRunning) { isRunning = true; state.frameLoader.subscribe(this); invalidateSelf(); } } private void stopRunning() { isRunning = false; state.frameLoader.unsubscribe(this); } @Override public boolean setVisible(boolean visible, boolean restart) { Preconditions.checkArgument( !isRecycled, "Cannot change the visibility of a recycled resource." + " Ensure that you unset the Drawable from your View before changing the View's" + " visibility."); isVisible = visible; if (!visible) { stopRunning(); } else if (isStarted) { startRunning(); } return super.setVisible(visible, restart); } @Override public int getIntrinsicWidth() { return state.frameLoader.getWidth(); } @Override public int getIntrinsicHeight() { return state.frameLoader.getHeight(); } @Override public boolean isRunning() { return isRunning; } // For testing. void setIsRunning(boolean isRunning) { this.isRunning = isRunning; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); applyGravity = true; } @Override public void draw(@NonNull Canvas canvas) { if (isRecycled) { return; } if (applyGravity) { Gravity.apply(GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), getDestRect()); applyGravity = false; } Bitmap currentFrame = state.frameLoader.getCurrentFrame(); canvas.drawBitmap(currentFrame, null, getDestRect(), getPaint()); } @Override public void setAlpha(int i) { getPaint().setAlpha(i); } @Override public void setColorFilter(ColorFilter colorFilter) { getPaint().setColorFilter(colorFilter); } private Rect getDestRect() { if (destRect == null) { destRect = new Rect(); } return destRect; } private Paint getPaint() { if (paint == null) { paint = new Paint(Paint.FILTER_BITMAP_FLAG); } return paint; } @Override public int getOpacity() { // We can't tell, so default to transparent to be safe. return PixelFormat.TRANSPARENT; } // See #1087. private Callback findCallback() { Callback callback = getCallback(); while (callback instanceof Drawable) { callback = ((Drawable) callback).getCallback(); } return callback; } @Override public void onFrameReady() { if (findCallback() == null) { stop(); invalidateSelf(); return; } invalidateSelf(); if (getFrameIndex() == getFrameCount() - 1) { loopCount++; } if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) { stop(); notifyAnimationEndToListeners(); } } private void notifyAnimationEndToListeners() { if (animationCallbacks != null) { for (int i = 0, size = animationCallbacks.size(); i < size; i++) { animationCallbacks.get(i).onAnimationEnd(this); } } } @Override public ConstantState getConstantState() { return state; } /** Clears any resources for loading frames that are currently held on to by this object. */ public void recycle() { isRecycled = true; state.frameLoader.clear(); } // For testing. boolean isRecycled() { return isRecycled; } // Public API. @SuppressWarnings("WeakerAccess") public void setLoopCount(int loopCount) { if (loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) { throw new IllegalArgumentException( "Loop count must be greater than 0, or equal to " + "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC"); } if (loopCount == LOOP_INTRINSIC) { int intrinsicCount = state.frameLoader.getLoopCount(); maxLoopCount = (intrinsicCount == TOTAL_ITERATION_COUNT_FOREVER) ? LOOP_FOREVER : intrinsicCount; } else { maxLoopCount = loopCount; } } /** * Register callback to listen to GifDrawable animation end event after specific loop count set by * {@link GifDrawable#setLoopCount(int)}. * *

    Note: This will only be called if the Gif stop because it reaches the loop count. Unregister * this in onLoadCleared to avoid potential memory leak. * * @see GifDrawable#unregisterAnimationCallback(AnimationCallback). * @param animationCallback Animation callback {@link Animatable2Compat.AnimationCallback}. */ @Override public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) { if (animationCallback == null) { return; } if (animationCallbacks == null) { animationCallbacks = new ArrayList<>(); } animationCallbacks.add(animationCallback); } @Override public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) { if (animationCallbacks == null || animationCallback == null) { return false; } return animationCallbacks.remove(animationCallback); } @Override public void clearAnimationCallbacks() { if (animationCallbacks != null) { animationCallbacks.clear(); } } static final class GifState extends ConstantState { @VisibleForTesting final GifFrameLoader frameLoader; GifState(GifFrameLoader frameLoader) { this.frameLoader = frameLoader; } @NonNull @Override public Drawable newDrawable(Resources res) { return newDrawable(); } @NonNull @Override public Drawable newDrawable() { return new GifDrawable(this); } @Override public int getChangingConfigurations() { return 0; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawableEncoder.java ================================================ package com.bumptech.glide.load.resource.gif; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.ByteBufferUtil; import java.io.File; import java.io.IOException; /** * Writes the original bytes of a {@link com.bumptech.glide.load.resource.gif.GifDrawable} to an * {@link java.io.OutputStream}. */ public class GifDrawableEncoder implements ResourceEncoder { private static final String TAG = "GifEncoder"; @NonNull @Override public EncodeStrategy getEncodeStrategy(@NonNull Options options) { return EncodeStrategy.SOURCE; } @Override public boolean encode( @NonNull Resource data, @NonNull File file, @NonNull Options options) { GifDrawable drawable = data.get(); boolean success = false; try { ByteBufferUtil.toFile(drawable.getBuffer(), file); success = true; } catch (IOException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to encode GIF drawable data", e); } } return success; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawableResource.java ================================================ package com.bumptech.glide.load.resource.gif; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.Initializable; import com.bumptech.glide.load.resource.drawable.DrawableResource; /** A resource wrapping an {@link com.bumptech.glide.load.resource.gif.GifDrawable}. */ public class GifDrawableResource extends DrawableResource implements Initializable { // Public API. @SuppressWarnings("WeakerAccess") public GifDrawableResource(GifDrawable drawable) { super(drawable); } @NonNull @Override public Class getResourceClass() { return GifDrawable.class; } @Override public int getSize() { return drawable.getSize(); } @Override public void recycle() { drawable.stop(); drawable.recycle(); } @Override public void initialize() { drawable.getFirstFrame().prepareToDraw(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifDrawableTransformation.java ================================================ package com.bumptech.glide.load.resource.gif; import android.content.Context; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.BitmapResource; import com.bumptech.glide.util.Preconditions; import java.security.MessageDigest; /** * An {@link com.bumptech.glide.load.Transformation} that wraps a transformation for a {@link * Bitmap} and can apply it to every frame of any {@link * com.bumptech.glide.load.resource.gif.GifDrawable}. */ public class GifDrawableTransformation implements Transformation { private final Transformation wrapped; public GifDrawableTransformation(Transformation wrapped) { this.wrapped = Preconditions.checkNotNull(wrapped); } @NonNull @Override public Resource transform( @NonNull Context context, @NonNull Resource resource, int outWidth, int outHeight) { GifDrawable drawable = resource.get(); // The drawable needs to be initialized with the correct width and height in order for a view // displaying it to end up with the right dimensions. Since our transformations may arbitrarily // modify the dimensions of our GIF, here we create a stand in for a frame and pass it to the // transformation to see what the final transformed dimensions will be so that our drawable can // report the correct intrinsic width and height. BitmapPool bitmapPool = Glide.get(context).getBitmapPool(); Bitmap firstFrame = drawable.getFirstFrame(); Resource bitmapResource = new BitmapResource(firstFrame, bitmapPool); Resource transformed = wrapped.transform(context, bitmapResource, outWidth, outHeight); if (!bitmapResource.equals(transformed)) { bitmapResource.recycle(); } Bitmap transformedFrame = transformed.get(); drawable.setFrameTransformation(wrapped, transformedFrame); return resource; } @Override public boolean equals(Object o) { if (o instanceof GifDrawableTransformation) { GifDrawableTransformation other = (GifDrawableTransformation) o; return wrapped.equals(other.wrapped); } return false; } @Override public int hashCode() { return wrapped.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { wrapped.updateDiskCacheKey(messageDigest); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameLoader.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.request.RequestOptions.diskCacheStrategyOf; import static com.bumptech.glide.request.RequestOptions.signatureOf; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; class GifFrameLoader { private final GifDecoder gifDecoder; private final Handler handler; private final List callbacks = new ArrayList<>(); @SuppressWarnings("WeakerAccess") @Synthetic final RequestManager requestManager; private final BitmapPool bitmapPool; private boolean isRunning; private boolean isLoadPending; private boolean startFromFirstFrame; private RequestBuilder requestBuilder; private DelayTarget current; private boolean isCleared; private DelayTarget next; private Bitmap firstFrame; private Transformation transformation; private DelayTarget pendingTarget; @Nullable private GifFrameLoader.OnEveryFrameListener onEveryFrameListener; private int firstFrameSize; private int width; private int height; public interface FrameCallback { void onFrameReady(); } GifFrameLoader( Glide glide, GifDecoder gifDecoder, int width, int height, Transformation transformation, Bitmap firstFrame) { this( glide.getBitmapPool(), Glide.with(glide.getContext()), gifDecoder, null /*handler*/, getRequestBuilder(Glide.with(glide.getContext()), width, height), transformation, firstFrame); } @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") GifFrameLoader( BitmapPool bitmapPool, RequestManager requestManager, GifDecoder gifDecoder, Handler handler, RequestBuilder requestBuilder, Transformation transformation, Bitmap firstFrame) { this.requestManager = requestManager; if (handler == null) { handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback()); } this.bitmapPool = bitmapPool; this.handler = handler; this.requestBuilder = requestBuilder; this.gifDecoder = gifDecoder; setFrameTransformation(transformation, firstFrame); } void setFrameTransformation(Transformation transformation, Bitmap firstFrame) { this.transformation = Preconditions.checkNotNull(transformation); this.firstFrame = Preconditions.checkNotNull(firstFrame); requestBuilder = requestBuilder.apply(new RequestOptions().transform(transformation)); firstFrameSize = Util.getBitmapByteSize(firstFrame); width = firstFrame.getWidth(); height = firstFrame.getHeight(); } Transformation getFrameTransformation() { return transformation; } Bitmap getFirstFrame() { return firstFrame; } void subscribe(FrameCallback frameCallback) { if (isCleared) { throw new IllegalStateException("Cannot subscribe to a cleared frame loader"); } if (callbacks.contains(frameCallback)) { throw new IllegalStateException("Cannot subscribe twice in a row"); } boolean start = callbacks.isEmpty(); callbacks.add(frameCallback); if (start) { start(); } } void unsubscribe(FrameCallback frameCallback) { callbacks.remove(frameCallback); if (callbacks.isEmpty()) { stop(); } } int getWidth() { return width; } int getHeight() { return height; } int getSize() { return gifDecoder.getByteSize() + firstFrameSize; } int getCurrentIndex() { return current != null ? current.index : -1; } ByteBuffer getBuffer() { return gifDecoder.getData().asReadOnlyBuffer(); } int getFrameCount() { return gifDecoder.getFrameCount(); } int getLoopCount() { return gifDecoder.getTotalIterationCount(); } private void start() { if (isRunning) { return; } isRunning = true; isCleared = false; loadNextFrame(); } private void stop() { isRunning = false; } void clear() { callbacks.clear(); recycleFirstFrame(); stop(); if (current != null) { requestManager.clear(current); current = null; } if (next != null) { requestManager.clear(next); next = null; } if (pendingTarget != null) { requestManager.clear(pendingTarget); pendingTarget = null; } gifDecoder.clear(); isCleared = true; } Bitmap getCurrentFrame() { return current != null ? current.getResource() : firstFrame; } private void loadNextFrame() { if (!isRunning || isLoadPending) { return; } if (startFromFirstFrame) { Preconditions.checkArgument( pendingTarget == null, "Pending target must be null when starting from the first frame"); gifDecoder.resetFrameIndex(); startFromFirstFrame = false; } if (pendingTarget != null) { DelayTarget temp = pendingTarget; pendingTarget = null; onFrameReady(temp); return; } isLoadPending = true; // Get the delay before incrementing the pointer because the delay indicates the amount of time // we want to spend on the current frame. int delay = gifDecoder.getNextDelay(); long targetTime = SystemClock.uptimeMillis() + delay; gifDecoder.advance(); next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime); requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next); } private void recycleFirstFrame() { if (firstFrame != null) { bitmapPool.put(firstFrame); firstFrame = null; } } void setNextStartFromFirstFrame() { Preconditions.checkArgument(!isRunning, "Can't restart a running animation"); startFromFirstFrame = true; if (pendingTarget != null) { requestManager.clear(pendingTarget); pendingTarget = null; } } @VisibleForTesting void setOnEveryFrameReadyListener(@Nullable OnEveryFrameListener onEveryFrameListener) { this.onEveryFrameListener = onEveryFrameListener; } @VisibleForTesting void onFrameReady(DelayTarget delayTarget) { if (onEveryFrameListener != null) { onEveryFrameListener.onFrameReady(); } isLoadPending = false; if (isCleared) { handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, delayTarget).sendToTarget(); return; } // If we're not running, notifying here will recycle the frame that we might currently be // showing, which breaks things (see #2526). We also can't discard this frame because we've // already incremented the frame pointer and can't decode the same frame again. Instead we'll // just hang on to this next frame until start() or clear() are called. if (!isRunning) { if (startFromFirstFrame) { handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, delayTarget).sendToTarget(); } else { pendingTarget = delayTarget; } return; } if (delayTarget.getResource() != null) { recycleFirstFrame(); DelayTarget previous = current; current = delayTarget; // The callbacks may unregister when onFrameReady is called, so iterate in reverse to avoid // concurrent modifications. for (int i = callbacks.size() - 1; i >= 0; i--) { FrameCallback cb = callbacks.get(i); cb.onFrameReady(); } if (previous != null) { handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, previous).sendToTarget(); } } loadNextFrame(); } private class FrameLoaderCallback implements Handler.Callback { static final int MSG_DELAY = 1; static final int MSG_CLEAR = 2; @Synthetic FrameLoaderCallback() {} @Override public boolean handleMessage(Message msg) { if (msg.what == MSG_DELAY) { GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj; onFrameReady(target); return true; } else if (msg.what == MSG_CLEAR) { GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj; requestManager.clear(target); } return false; } } @VisibleForTesting static class DelayTarget extends CustomTarget { private final Handler handler; @Synthetic final int index; private final long targetTime; private Bitmap resource; DelayTarget(Handler handler, int index, long targetTime) { this.handler = handler; this.index = index; this.targetTime = targetTime; } Bitmap getResource() { return resource; } @Override public void onResourceReady( @NonNull Bitmap resource, @Nullable Transition transition) { this.resource = resource; Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this); handler.sendMessageAtTime(msg, targetTime); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { this.resource = null; } } private static RequestBuilder getRequestBuilder( RequestManager requestManager, int width, int height) { return requestManager .asBitmap() .apply( diskCacheStrategyOf(DiskCacheStrategy.NONE) .useAnimationPool(true) .skipMemoryCache(true) .override(width, height)); } private static Key getFrameSignature() { // Some devices seem to have crypto bugs that throw exceptions when you create a new UUID. // See #1510. return new ObjectKey(Math.random()); } @VisibleForTesting interface OnEveryFrameListener { void onFrameReady(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java ================================================ package com.bumptech.glide.load.resource.gif; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.BitmapResource; /** * Decodes {@link Bitmap}s from {@link GifDecoder}s representing a particular frame of a particular * GIF image. */ public final class GifFrameResourceDecoder implements ResourceDecoder { private final BitmapPool bitmapPool; public GifFrameResourceDecoder(BitmapPool bitmapPool) { this.bitmapPool = bitmapPool; } @Override public boolean handles(@NonNull GifDecoder source, @NonNull Options options) { return true; } @Override public Resource decode( @NonNull GifDecoder source, int width, int height, @NonNull Options options) { Bitmap bitmap = source.getNextFrame(); return BitmapResource.obtain(bitmap, bitmapPool); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/GifOptions.java ================================================ package com.bumptech.glide.load.resource.gif; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; /** Options related to decoding GIFs. */ public final class GifOptions { /** * Indicates the {@link com.bumptech.glide.load.DecodeFormat} that will be used in conjunction * with the particular GIF to determine the {@link android.graphics.Bitmap.Config} to use when * decoding frames of GIFs. */ public static final Option DECODE_FORMAT = Option.memory( "com.bumptech.glide.load.resource.gif.GifOptions.DecodeFormat", DecodeFormat.DEFAULT); /** * If set to {@code true}, disables the GIF {@link com.bumptech.glide.load.ResourceDecoder}s * ({@link ResourceDecoder#handles(Object, Options)} will return {@code false}). Defaults to * {@code false}. */ public static final Option DISABLE_ANIMATION = Option.memory("com.bumptech.glide.load.resource.gif.GifOptions.DisableAnimation", false); private GifOptions() { // Utility class. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/gif/StreamGifDecoder.java ================================================ package com.bumptech.glide.load.resource.gif; import android.util.Log; import androidx.annotation.NonNull; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.List; /** * A relatively inefficient decoder for {@link com.bumptech.glide.load.resource.gif.GifDrawable} * that converts {@link java.io.InputStream}s to {@link java.nio.ByteBuffer}s and then passes the * buffer to a wrapped decoder. */ public class StreamGifDecoder implements ResourceDecoder { private static final String TAG = "StreamGifDecoder"; private final List parsers; private final ResourceDecoder byteBufferDecoder; private final ArrayPool byteArrayPool; public StreamGifDecoder( List parsers, ResourceDecoder byteBufferDecoder, ArrayPool byteArrayPool) { this.parsers = parsers; this.byteBufferDecoder = byteBufferDecoder; this.byteArrayPool = byteArrayPool; } @Override public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException { return !options.get(GifOptions.DISABLE_ANIMATION) && ImageHeaderParserUtils.getType(parsers, source, byteArrayPool) == ImageType.GIF; } @Override public Resource decode( @NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException { byte[] data = inputStreamToBytes(source); if (data == null) { return null; } ByteBuffer byteBuffer = ByteBuffer.wrap(data); return byteBufferDecoder.decode(byteBuffer, width, height, options); } private static byte[] inputStreamToBytes(InputStream is) { final int bufferSize = 16384; ByteArrayOutputStream buffer = new ByteArrayOutputStream(bufferSize); try { int nRead; byte[] data = new byte[bufferSize]; while ((nRead = is.read(data)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); } catch (IOException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Error reading data from stream", e); } return null; } return buffer.toByteArray(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/transcode/BitmapBytesTranscoder.java ================================================ package com.bumptech.glide.load.resource.transcode; import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.resource.bytes.BytesResource; import java.io.ByteArrayOutputStream; /** * An {@link com.bumptech.glide.load.resource.transcode.ResourceTranscoder} that converts {@link * android.graphics.Bitmap}s into byte arrays using {@link android.graphics.Bitmap#compress * (android.graphics.Bitmap.CompressFormat, int, java.io.OutputStream)}. */ public class BitmapBytesTranscoder implements ResourceTranscoder { private final Bitmap.CompressFormat compressFormat; private final int quality; public BitmapBytesTranscoder() { this(Bitmap.CompressFormat.JPEG, 100); } // Public API. @SuppressWarnings("WeakerAccess") public BitmapBytesTranscoder(@NonNull Bitmap.CompressFormat compressFormat, int quality) { this.compressFormat = compressFormat; this.quality = quality; } @Nullable @Override public Resource transcode( @NonNull Resource toTranscode, @NonNull Options options) { ByteArrayOutputStream os = new ByteArrayOutputStream(); toTranscode.get().compress(compressFormat, quality, os); toTranscode.recycle(); return new BytesResource(os.toByteArray()); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/transcode/BitmapDrawableTranscoder.java ================================================ package com.bumptech.glide.load.resource.transcode; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.LazyBitmapDrawableResource; import com.bumptech.glide.util.Preconditions; /** * An {@link com.bumptech.glide.load.resource.transcode.ResourceTranscoder} that converts {@link * android.graphics.Bitmap}s into {@link android.graphics.drawable.BitmapDrawable}s. */ public class BitmapDrawableTranscoder implements ResourceTranscoder { private final Resources resources; // Public API. @SuppressWarnings("unused") public BitmapDrawableTranscoder(@NonNull Context context) { this(context.getResources()); } /** * @deprecated Use {@link #BitmapDrawableTranscoder(Resources)}, {@code bitmapPool} is unused. */ @Deprecated public BitmapDrawableTranscoder( @NonNull Resources resources, @SuppressWarnings("unused") BitmapPool bitmapPool) { this(resources); } public BitmapDrawableTranscoder(@NonNull Resources resources) { this.resources = Preconditions.checkNotNull(resources); } @Nullable @Override public Resource transcode( @NonNull Resource toTranscode, @NonNull Options options) { return LazyBitmapDrawableResource.obtain(resources, toTranscode); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/transcode/DrawableBytesTranscoder.java ================================================ package com.bumptech.glide.load.resource.transcode; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.BitmapResource; import com.bumptech.glide.load.resource.gif.GifDrawable; /** * Obtains {@code byte[]} from {@link BitmapDrawable}s by delegating to a {@link ResourceTranscoder} * for {@link Bitmap}s to {@code byte[]}s. */ public final class DrawableBytesTranscoder implements ResourceTranscoder { private final BitmapPool bitmapPool; private final ResourceTranscoder bitmapBytesTranscoder; private final ResourceTranscoder gifDrawableBytesTranscoder; public DrawableBytesTranscoder( @NonNull BitmapPool bitmapPool, @NonNull ResourceTranscoder bitmapBytesTranscoder, @NonNull ResourceTranscoder gifDrawableBytesTranscoder) { this.bitmapPool = bitmapPool; this.bitmapBytesTranscoder = bitmapBytesTranscoder; this.gifDrawableBytesTranscoder = gifDrawableBytesTranscoder; } @Nullable @Override public Resource transcode( @NonNull Resource toTranscode, @NonNull Options options) { Drawable drawable = toTranscode.get(); if (drawable instanceof BitmapDrawable) { return bitmapBytesTranscoder.transcode( BitmapResource.obtain(((BitmapDrawable) drawable).getBitmap(), bitmapPool), options); } else if (drawable instanceof GifDrawable) { return gifDrawableBytesTranscoder.transcode(toGifDrawableResource(toTranscode), options); } return null; } @SuppressWarnings("unchecked") @NonNull private static Resource toGifDrawableResource(@NonNull Resource resource) { return (Resource) (Resource) resource; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/transcode/GifDrawableBytesTranscoder.java ================================================ package com.bumptech.glide.load.resource.transcode; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.resource.bytes.BytesResource; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.util.ByteBufferUtil; import java.nio.ByteBuffer; /** * An {@link com.bumptech.glide.load.resource.transcode.ResourceTranscoder} that converts {@link * com.bumptech.glide.load.resource.gif.GifDrawable} into bytes by obtaining the original bytes of * the GIF from the {@link com.bumptech.glide.load.resource.gif.GifDrawable}. */ public class GifDrawableBytesTranscoder implements ResourceTranscoder { @Nullable @Override public Resource transcode( @NonNull Resource toTranscode, @NonNull Options options) { GifDrawable gifData = toTranscode.get(); ByteBuffer byteBuffer = gifData.getBuffer(); return new BytesResource(ByteBufferUtil.toBytes(byteBuffer)); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/transcode/ResourceTranscoder.java ================================================ package com.bumptech.glide.load.resource.transcode; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; /** * Transcodes a resource of one type to a resource of another type. * * @param The type of the resource that will be transcoded from. * @param The type of the resource that will be transcoded to. */ public interface ResourceTranscoder { /** * Transcodes the given resource to the new resource type and returns the new resource. * * @param toTranscode The resource to transcode. */ @Nullable Resource transcode(@NonNull Resource toTranscode, @NonNull Options options); } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/transcode/TranscoderRegistry.java ================================================ package com.bumptech.glide.load.resource.transcode; import androidx.annotation.NonNull; import com.bumptech.glide.util.Synthetic; import java.util.ArrayList; import java.util.List; /** * A class that allows {@link com.bumptech.glide.load.resource.transcode.ResourceTranscoder}s to be * registered and retrieved by the classes they convert between. */ public class TranscoderRegistry { private final List> transcoders = new ArrayList<>(); /** * Registers the given {@link com.bumptech.glide.load.resource.transcode.ResourceTranscoder} using * the given classes so it can later be retrieved using the given classes. * * @param decodedClass The class of the resource that the transcoder transcodes from. * @param transcodedClass The class of the resource that the transcoder transcodes to. * @param transcoder The transcoder. * @param The type of the resource that the transcoder transcodes from. * @param The type of the resource that the transcoder transcodes to. */ public synchronized void register( @NonNull Class decodedClass, @NonNull Class transcodedClass, @NonNull ResourceTranscoder transcoder) { transcoders.add(new Entry<>(decodedClass, transcodedClass, transcoder)); } /** * Returns the currently registered {@link * com.bumptech.glide.load.resource.transcode.ResourceTranscoder} for the given classes. * * @param resourceClass The class of the resource that the transcoder transcodes from. * @param transcodedClass The class of the resource that the transcoder transcodes to. * @param The type of the resource that the transcoder transcodes from. * @param The type of the resource that the transcoder transcodes to. */ @NonNull @SuppressWarnings("unchecked") public synchronized ResourceTranscoder get( @NonNull Class resourceClass, @NonNull Class transcodedClass) { // For example, there may be a transcoder that can convert a GifDrawable to a Drawable, which // will be caught above. However, if there is no registered transcoder, we can still just use // the UnitTranscoder to return the Drawable because the transcode class (Drawable) is // assignable from the resource class (GifDrawable). if (transcodedClass.isAssignableFrom(resourceClass)) { return (ResourceTranscoder) UnitTranscoder.get(); } for (Entry entry : transcoders) { if (entry.handles(resourceClass, transcodedClass)) { return (ResourceTranscoder) entry.transcoder; } } throw new IllegalArgumentException( "No transcoder registered to transcode from " + resourceClass + " to " + transcodedClass); } @NonNull @SuppressWarnings("unchecked") public synchronized List> getTranscodeClasses( @NonNull Class resourceClass, @NonNull Class transcodeClass) { List> transcodeClasses = new ArrayList<>(); // GifDrawable -> Drawable is just the UnitTranscoder, as is GifDrawable -> GifDrawable. if (transcodeClass.isAssignableFrom(resourceClass)) { transcodeClasses.add(transcodeClass); return transcodeClasses; } for (Entry entry : transcoders) { if (entry.handles(resourceClass, transcodeClass) && !transcodeClasses.contains((Class) entry.toClass)) { transcodeClasses.add((Class) entry.toClass); } } return transcodeClasses; } private static final class Entry { @Synthetic final Class fromClass; @Synthetic final Class toClass; @Synthetic final ResourceTranscoder transcoder; Entry( @NonNull Class fromClass, @NonNull Class toClass, @NonNull ResourceTranscoder transcoder) { this.fromClass = fromClass; this.toClass = toClass; this.transcoder = transcoder; } /** * If we convert from a specific Drawable, we must get that specific Drawable class or a * subclass of that Drawable. In contrast, if we we convert to a specific Drawable, we * can fulfill requests for a more generic parent class (like Drawable). As a result, we check * fromClass and toClass in different orders. */ public boolean handles(@NonNull Class fromClass, @NonNull Class toClass) { return this.fromClass.isAssignableFrom(fromClass) && toClass.isAssignableFrom(this.toClass); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/load/resource/transcode/UnitTranscoder.java ================================================ package com.bumptech.glide.load.resource.transcode; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; /** * A simple {@link ResourceTranscoder} that simply returns the given resource. * * @param The type of the resource that will be transcoded from and to. */ public class UnitTranscoder implements ResourceTranscoder { private static final UnitTranscoder UNIT_TRANSCODER = new UnitTranscoder<>(); @SuppressWarnings("unchecked") public static ResourceTranscoder get() { return (ResourceTranscoder) UNIT_TRANSCODER; } @Nullable @Override public Resource transcode(@NonNull Resource toTranscode, @NonNull Options options) { return toTranscode; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/ApplicationLifecycle.java ================================================ package com.bumptech.glide.manager; import androidx.annotation.NonNull; /** * A {@link com.bumptech.glide.manager.Lifecycle} implementation for tracking and notifying * listeners of {@link android.app.Application} lifecycle events. * *

    Since there are essentially no {@link android.app.Application} lifecycle events, this class * simply defaults to notifying new listeners that they are started. */ class ApplicationLifecycle implements Lifecycle { @Override public void addListener(@NonNull LifecycleListener listener) { listener.onStart(); } @Override public void removeListener(@NonNull LifecycleListener listener) { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/ConnectivityMonitor.java ================================================ package com.bumptech.glide.manager; /** An interface for monitoring network connectivity events. */ public interface ConnectivityMonitor extends LifecycleListener { /** An interface for listening to network connectivity events picked up by the monitor. */ interface ConnectivityListener { /** * Called when the connectivity state changes. * * @param isConnected True if we're currently connected to a network, false otherwise. */ void onConnectivityChanged(boolean isConnected); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/ConnectivityMonitorFactory.java ================================================ package com.bumptech.glide.manager; import android.content.Context; import androidx.annotation.NonNull; /** * A factory class that produces a functional {@link * com.bumptech.glide.manager.ConnectivityMonitor}. */ public interface ConnectivityMonitorFactory { @NonNull ConnectivityMonitor build( @NonNull Context context, @NonNull ConnectivityMonitor.ConnectivityListener listener); } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/DefaultConnectivityMonitor.java ================================================ package com.bumptech.glide.manager; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.util.Synthetic; /** * An Android Lifecycle wrapper that uses {@link SingletonConnectivityReceiver} to observer * connectivity changes, allowing for registration to be removed when our listener is being * destroyed as part of the Android lifecycle. */ final class DefaultConnectivityMonitor implements ConnectivityMonitor { private final Context context; @SuppressWarnings("WeakerAccess") @Synthetic final ConnectivityListener listener; DefaultConnectivityMonitor(@NonNull Context context, @NonNull ConnectivityListener listener) { this.context = context.getApplicationContext(); this.listener = listener; } private void register() { SingletonConnectivityReceiver.get(context).register(listener); } private void unregister() { SingletonConnectivityReceiver.get(context).unregister(listener); } @Override public void onStart() { register(); } @Override public void onStop() { unregister(); } @Override public void onDestroy() { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/DefaultConnectivityMonitorFactory.java ================================================ package com.bumptech.glide.manager; import android.content.Context; import android.content.pm.PackageManager; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; /** * A factory class that produces a functional {@link com.bumptech.glide.manager.ConnectivityMonitor} * if the application has the {@code android.permission.ACCESS_NETWORK_STATE} permission and a no-op * non functional {@link com.bumptech.glide.manager.ConnectivityMonitor} if the app does not have * the required permission. */ public class DefaultConnectivityMonitorFactory implements ConnectivityMonitorFactory { private static final String TAG = "ConnectivityMonitor"; private static final String NETWORK_PERMISSION = "android.permission.ACCESS_NETWORK_STATE"; @NonNull @Override public ConnectivityMonitor build( @NonNull Context context, @NonNull ConnectivityMonitor.ConnectivityListener listener) { int permissionResult = ContextCompat.checkSelfPermission(context, NETWORK_PERMISSION); boolean hasPermission = permissionResult == PackageManager.PERMISSION_GRANTED; if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, hasPermission ? "ACCESS_NETWORK_STATE permission granted, registering connectivity monitor" : "ACCESS_NETWORK_STATE permission missing, cannot register connectivity monitor"); } return hasPermission ? new DefaultConnectivityMonitor(context, listener) : new NullConnectivityMonitor(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/DoNothingFirstFrameWaiter.java ================================================ package com.bumptech.glide.manager; import android.app.Activity; final class DoNothingFirstFrameWaiter implements FrameWaiter { @Override public void registerSelf(Activity activity) { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/EmptyRequestManagerTreeNode.java ================================================ package com.bumptech.glide.manager; import androidx.annotation.NonNull; import com.bumptech.glide.RequestManager; import java.util.Collections; import java.util.Set; /** A {@link RequestManagerTreeNode} that returns no relatives. */ final class EmptyRequestManagerTreeNode implements RequestManagerTreeNode { @NonNull @Override public Set getDescendants() { return Collections.emptySet(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/FirstFrameWaiter.java ================================================ package com.bumptech.glide.manager; import android.app.Activity; import android.os.Build; import android.view.View; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnDrawListener; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.resource.bitmap.HardwareConfigState; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; @RequiresApi(Build.VERSION_CODES.O) final class FirstFrameWaiter implements FrameWaiter { @Synthetic final Set pendingActivities = Collections.newSetFromMap(new WeakHashMap()); @Synthetic volatile boolean isFirstFrameSet; @Override public void registerSelf(Activity activity) { // It's possible we'll create a few of these, but it's not particularly expensive to do so and // we'd rather work around any edge cases that might prevent the first Activity we listen to // from firing our callback ever. if (isFirstFrameSet) { return; } if (!pendingActivities.add(activity)) { return; } final View view = activity.getWindow().getDecorView(); ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); viewTreeObserver.addOnDrawListener( new OnDrawListener() { @Override public void onDraw() { // We can't remove the listener during onDraw, so always post the removal to the UI // thread, even if the first frame may already be set before our listener goes off. final OnDrawListener listener = this; Util.postOnUiThread( new Runnable() { @Override public void run() { HardwareConfigState.getInstance().unblockHardwareBitmaps(); isFirstFrameSet = true; removeListener(view, listener); pendingActivities.clear(); } }); } }); } @Synthetic static void removeListener(View view, OnDrawListener listener) { // The original ViewTreeObserver might be merged into a new one and be dead. // Since we have to handle that case anyway, We might as well always just // obtain the current observer and use a single code path. // We also have to wait to remove this because we're being called in onDraw. ViewTreeObserver currentViewTreeObserver = view.getViewTreeObserver(); currentViewTreeObserver.removeOnDrawListener(listener); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/FrameWaiter.java ================================================ package com.bumptech.glide.manager; import android.app.Activity; interface FrameWaiter { void registerSelf(Activity activity); } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/Lifecycle.java ================================================ package com.bumptech.glide.manager; import androidx.annotation.NonNull; /** An interface for listening to Activity/Fragment lifecycle events. */ public interface Lifecycle { /** Adds the given listener to the set of listeners managed by this Lifecycle implementation. */ void addListener(@NonNull LifecycleListener listener); /** * Removes the given listener from the set of listeners managed by this Lifecycle implementation, * returning {@code true} if the listener was removed successfully, and {@code false} otherwise. * *

    This is an optimization only, there is no guarantee that every added listener will * eventually be removed. */ void removeListener(@NonNull LifecycleListener listener); } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/LifecycleLifecycle.java ================================================ package com.bumptech.glide.manager; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle.Event; import androidx.lifecycle.Lifecycle.State; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.OnLifecycleEvent; import com.bumptech.glide.util.Util; import java.util.HashSet; import java.util.Set; @SuppressWarnings("OnLifecycleEvent") // Glide doesn't support Java 8 final class LifecycleLifecycle implements Lifecycle, LifecycleObserver { @NonNull private final Set lifecycleListeners = new HashSet(); @NonNull private final androidx.lifecycle.Lifecycle lifecycle; LifecycleLifecycle(androidx.lifecycle.Lifecycle lifecycle) { this.lifecycle = lifecycle; lifecycle.addObserver(this); } @OnLifecycleEvent(Event.ON_START) public void onStart(@NonNull LifecycleOwner owner) { for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStart(); } } @OnLifecycleEvent(Event.ON_STOP) public void onStop(@NonNull LifecycleOwner owner) { for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStop(); } } @OnLifecycleEvent(Event.ON_DESTROY) public void onDestroy(@NonNull LifecycleOwner owner) { for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onDestroy(); } owner.getLifecycle().removeObserver(this); } @Override public void addListener(@NonNull LifecycleListener listener) { lifecycleListeners.add(listener); if (lifecycle.getCurrentState() == State.DESTROYED) { listener.onDestroy(); } else if (lifecycle.getCurrentState().isAtLeast(State.STARTED)) { listener.onStart(); } else { listener.onStop(); } } @Override public void removeListener(@NonNull LifecycleListener listener) { lifecycleListeners.remove(listener); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/LifecycleListener.java ================================================ package com.bumptech.glide.manager; /** * An interface for listener to {@link android.app.Fragment} and {@link android.app.Activity} * lifecycle events. */ public interface LifecycleListener { /** * Callback for when {@link android.app.Fragment#onStart()}} or {@link * android.app.Activity#onStart()} is called. */ void onStart(); /** * Callback for when {@link android.app.Fragment#onStop()}} or {@link * android.app.Activity#onStop()}} is called. */ void onStop(); /** * Callback for when {@link android.app.Fragment#onDestroy()}} or {@link * android.app.Activity#onDestroy()} is called. */ void onDestroy(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/LifecycleRequestManagerRetriever.java ================================================ package com.bumptech.glide.manager; import android.content.Context; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.Lifecycle; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.manager.RequestManagerRetriever.RequestManagerFactory; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; final class LifecycleRequestManagerRetriever { @Synthetic final Map lifecycleToRequestManager = new HashMap<>(); @NonNull private final RequestManagerFactory factory; LifecycleRequestManagerRetriever(@NonNull RequestManagerFactory factory) { this.factory = factory; } RequestManager getOnly(Lifecycle lifecycle) { Util.assertMainThread(); return lifecycleToRequestManager.get(lifecycle); } RequestManager getOrCreate( Context context, Glide glide, final Lifecycle lifecycle, FragmentManager childFragmentManager, boolean isParentVisible) { Util.assertMainThread(); RequestManager result = getOnly(lifecycle); if (result == null) { LifecycleLifecycle glideLifecycle = new LifecycleLifecycle(lifecycle); result = factory.build( glide, glideLifecycle, new SupportRequestManagerTreeNode(childFragmentManager), context); lifecycleToRequestManager.put(lifecycle, result); glideLifecycle.addListener( new LifecycleListener() { @Override public void onStart() {} @Override public void onStop() {} @Override public void onDestroy() { lifecycleToRequestManager.remove(lifecycle); } }); // This is a bit of hack, we're going to start the RequestManager, but not the // corresponding Lifecycle. It's safe to start the RequestManager, but starting the // Lifecycle might trigger memory leaks. See b/154405040 if (isParentVisible) { result.onStart(); } } return result; } private final class SupportRequestManagerTreeNode implements RequestManagerTreeNode { private final FragmentManager childFragmentManager; SupportRequestManagerTreeNode(FragmentManager childFragmentManager) { this.childFragmentManager = childFragmentManager; } @NonNull @Override public Set getDescendants() { Set result = new HashSet<>(); getChildFragmentsRecursive(childFragmentManager, result); return result; } private void getChildFragmentsRecursive( FragmentManager fragmentManager, Set requestManagers) { List children = fragmentManager.getFragments(); for (int i = 0, size = children.size(); i < size; i++) { Fragment child = children.get(i); getChildFragmentsRecursive(child.getChildFragmentManager(), requestManagers); RequestManager fromChild = getOnly(child.getLifecycle()); if (fromChild != null) { requestManagers.add(fromChild); } } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/NullConnectivityMonitor.java ================================================ package com.bumptech.glide.manager; /** A no-op {@link com.bumptech.glide.manager.ConnectivityMonitor}. */ class NullConnectivityMonitor implements ConnectivityMonitor { @Override public void onStart() { // Do nothing. } @Override public void onStop() { // Do nothing. } @Override public void onDestroy() { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/RequestManagerFragment.java ================================================ package com.bumptech.glide.manager; import android.app.Fragment; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.RequestManager; import java.util.Collections; import java.util.Set; /** * @deprecated This class is unused by Glide and contains only no-op methods. It's retained along * with its public methods to avoid breaking binary compatibility. Lifecycle integration is no * longer supported outside of androidx Activitys and Fragments. */ @Deprecated public class RequestManagerFragment extends Fragment { /** * @deprecated This method is a no-op. See the class comment for deprecation details. */ @Deprecated public void setRequestManager(@Nullable RequestManager requestManager) {} /** * @deprecated This always returns null. See the class comment for deprecation details. */ @Deprecated @Nullable public RequestManager getRequestManager() { return null; } /** * @deprecated This always returns an empty tree node. See the class comment for deprecation * details. */ @Deprecated @NonNull public RequestManagerTreeNode getRequestManagerTreeNode() { return new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return Collections.emptySet(); } }; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/RequestManagerRetriever.java ================================================ package com.bumptech.glide.manager; import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.ContextWrapper; import android.os.Build; import android.os.Handler; import android.os.Message; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.collection.ArrayMap; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.resource.bitmap.HardwareConfigState; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; import java.util.Collection; import java.util.Map; /** * A collection of static methods for creating new {@link com.bumptech.glide.RequestManager}s or * retrieving existing ones from activities and fragment. */ public class RequestManagerRetriever implements Handler.Callback { @VisibleForTesting static final String FRAGMENT_TAG = "com.bumptech.glide.manager"; /** The top application level RequestManager. */ private volatile RequestManager applicationManager; private final RequestManagerFactory factory; // Objects used to find Fragments and Activities containing views. private final ArrayMap tempViewToSupportFragment = new ArrayMap<>(); // This is really misplaced here, but to put it anywhere else means duplicating all of the // Fragment/Activity extraction logic that already exists here. It's gross, but less likely to // break. private final FrameWaiter frameWaiter; private final LifecycleRequestManagerRetriever lifecycleRequestManagerRetriever; public RequestManagerRetriever(@Nullable RequestManagerFactory factory) { this.factory = factory != null ? factory : DEFAULT_FACTORY; lifecycleRequestManagerRetriever = new LifecycleRequestManagerRetriever(this.factory); frameWaiter = buildFrameWaiter(); } private static FrameWaiter buildFrameWaiter() { if (!HardwareConfigState.HARDWARE_BITMAPS_SUPPORTED || !HardwareConfigState.BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED) { return new DoNothingFirstFrameWaiter(); } return new FirstFrameWaiter(); } @NonNull private RequestManager getApplicationManager(@NonNull Context context) { // Either an application context or we're on a background thread. if (applicationManager == null) { synchronized (this) { if (applicationManager == null) { // Normally pause/resume is taken care of by the fragment we add to the fragment or // activity. However, in this case since the manager attached to the application will not // receive lifecycle events, we must force the manager to start resumed using // ApplicationLifecycle. // TODO(b/27524013): Factor out this Glide.get() call. Glide glide = Glide.get(context.getApplicationContext()); applicationManager = factory.build( glide, new ApplicationLifecycle(), new EmptyRequestManagerTreeNode(), context.getApplicationContext()); } } } return applicationManager; } @NonNull public RequestManager get(@NonNull Context context) { if (context == null) { throw new IllegalArgumentException("You cannot start a load on a null Context"); } else if (Util.isOnMainThread() && !(context instanceof Application)) { if (context instanceof FragmentActivity) { return get((FragmentActivity) context); } else if (context instanceof ContextWrapper // Only unwrap a ContextWrapper if the baseContext has a non-null application context. // Context#createPackageContext may return a Context without an Application instance, // in which case a ContextWrapper may be used to attach one. && ((ContextWrapper) context).getBaseContext().getApplicationContext() != null) { return get(((ContextWrapper) context).getBaseContext()); } } return getApplicationManager(context); } @NonNull public RequestManager get(@NonNull FragmentActivity activity) { if (Util.isOnBackgroundThread()) { return get(activity.getApplicationContext()); } assertNotDestroyed(activity); frameWaiter.registerSelf(activity); boolean isActivityVisible = isActivityVisible(activity); Glide glide = Glide.get(activity.getApplicationContext()); return lifecycleRequestManagerRetriever.getOrCreate( activity, glide, activity.getLifecycle(), activity.getSupportFragmentManager(), isActivityVisible); } @NonNull public RequestManager get(@NonNull Fragment fragment) { Preconditions.checkNotNull( fragment.getContext(), "You cannot start a load on a fragment before it is attached or after it is destroyed"); if (Util.isOnBackgroundThread()) { return get(fragment.getContext().getApplicationContext()); } // In some unusual cases, it's possible to have a Fragment not hosted by an activity. There's // not all that much we can do here. Most apps will be started with a standard activity. If // we manage not to register the first frame waiter for a while, the consequences are not // catastrophic, we'll just use some extra memory. if (fragment.getActivity() != null) { frameWaiter.registerSelf(fragment.getActivity()); } FragmentManager fm = fragment.getChildFragmentManager(); Context context = fragment.getContext(); Glide glide = Glide.get(context.getApplicationContext()); return lifecycleRequestManagerRetriever.getOrCreate( context, glide, fragment.getLifecycle(), fm, fragment.isVisible()); } /** * @deprecated This is identical to calling {@link #get(Context)} with the application context. * Use androidx Activities instead (ie {@link FragmentActivity}, or {@link * androidx.appcompat.app.AppCompatActivity}). */ @Deprecated @NonNull public RequestManager get(@NonNull Activity activity) { return get(activity.getApplicationContext()); } @NonNull public RequestManager get(@NonNull View view) { if (Util.isOnBackgroundThread()) { return get(view.getContext().getApplicationContext()); } Preconditions.checkNotNull(view); Preconditions.checkNotNull( view.getContext(), "Unable to obtain a request manager for a view without a Context"); Activity activity = findActivity(view.getContext()); // The view might be somewhere else, like a service. if (activity == null) { return get(view.getContext().getApplicationContext()); } // Support Fragments. // Although the user might have non-support Fragments attached to FragmentActivity, searching // for non-support Fragments is so expensive pre O and that should be rare enough that we // prefer to just fall back to the Activity directly. if (activity instanceof FragmentActivity) { Fragment fragment = findSupportFragment(view, (FragmentActivity) activity); return fragment != null ? get(fragment) : get((FragmentActivity) activity); } // Standard Fragments. return get(view.getContext().getApplicationContext()); } private static void findAllSupportFragmentsWithViews( @Nullable Collection topLevelFragments, @NonNull Map result) { if (topLevelFragments == null) { return; } for (Fragment fragment : topLevelFragments) { // getFragment()s in the support FragmentManager may contain null values, see #1991. if (fragment == null || fragment.getView() == null) { continue; } result.put(fragment.getView(), fragment); findAllSupportFragmentsWithViews(fragment.getChildFragmentManager().getFragments(), result); } } @Nullable private Fragment findSupportFragment(@NonNull View target, @NonNull FragmentActivity activity) { tempViewToSupportFragment.clear(); findAllSupportFragmentsWithViews( activity.getSupportFragmentManager().getFragments(), tempViewToSupportFragment); Fragment result = null; View activityRoot = activity.findViewById(android.R.id.content); View current = target; while (!current.equals(activityRoot)) { result = tempViewToSupportFragment.get(current); if (result != null) { break; } if (current.getParent() instanceof View) { current = (View) current.getParent(); } else { break; } } tempViewToSupportFragment.clear(); return result; } @Nullable private static Activity findActivity(@NonNull Context context) { if (context instanceof Activity) { return (Activity) context; } else if (context instanceof ContextWrapper) { return findActivity(((ContextWrapper) context).getBaseContext()); } else { return null; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private static void assertNotDestroyed(@NonNull Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) { throw new IllegalArgumentException("You cannot start a load for a destroyed activity"); } } /** * @deprecated This is equivalent to calling {@link #get(Context)} with the application context. * Use androidx fragments instead: {@link Fragment}. */ @Deprecated @NonNull @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public RequestManager get(@NonNull android.app.Fragment fragment) { if (fragment.getActivity() == null) { throw new IllegalArgumentException( "You cannot start a load on a fragment before it is attached"); } return get(fragment.getActivity().getApplicationContext()); } private static boolean isActivityVisible(Context context) { // This is a poor heuristic, but it's about all we have. We'd rather err on the side of visible // and start requests than on the side of invisible and ignore valid requests. Activity activity = findActivity(context); return activity == null || !activity.isFinishing(); } /** * @deprecated This method is no longer called by Glide or provides any functionality and it will * be removed in the future. Retained for now to preserve backwards compatibility. */ @Deprecated @SuppressWarnings("PMD.CollapsibleIfStatements") @Override public boolean handleMessage(Message message) { return false; } /** Used internally to create {@link RequestManager}s. */ public interface RequestManagerFactory { @NonNull RequestManager build( @NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context); } private static final RequestManagerFactory DEFAULT_FACTORY = new RequestManagerFactory() { @NonNull @Override public RequestManager build( @NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context) { return new RequestManager(glide, lifecycle, requestManagerTreeNode, context); } }; } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/RequestManagerTreeNode.java ================================================ package com.bumptech.glide.manager; import androidx.annotation.NonNull; import com.bumptech.glide.RequestManager; import java.util.Set; /** * Provides access to the relatives of a RequestManager based on the current context. The context * hierarchy is provided by nesting in Activity and Fragments; the application context does not * provide access to any other RequestManagers hierarchically. */ public interface RequestManagerTreeNode { /** * Returns all descendant {@link RequestManager}s relative to the context of the current {@link * RequestManager}. */ @NonNull Set getDescendants(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/RequestTracker.java ================================================ package com.bumptech.glide.manager; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.request.Request; import com.bumptech.glide.util.Util; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.WeakHashMap; /** * A class for tracking, canceling, and restarting in progress, completed, and failed requests. * *

    This class is not thread safe and must be accessed on the main thread. */ public class RequestTracker { private static final String TAG = "RequestTracker"; // Most requests will be for views and will therefore be held strongly (and safely) by the view // via the tag. However, a user can always pass in a different type of target which may end up not // being strongly referenced even though the user still would like the request to finish. Weak // references are therefore only really functional in this context for view targets. Despite the // side affects, WeakReferences are still essentially required. A user can always make repeated // requests into targets other than views, or use an activity manager in a fragment pager where // holding strong references would steadily leak bitmaps and/or views. private final Set requests = Collections.newSetFromMap(new WeakHashMap()); // A set of requests that have not completed and are queued to be run again. We use this list to // maintain hard references to these requests to ensure that they are not garbage collected // before they start running or while they are paused. See #346. private final Set pendingRequests = new HashSet<>(); private boolean isPaused; /** Starts tracking the given request. */ public void runRequest(@NonNull Request request) { requests.add(request); if (!isPaused) { request.begin(); } else { request.clear(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Paused, delaying request"); } pendingRequests.add(request); } } @VisibleForTesting void addRequest(Request request) { requests.add(request); } /** * Stops tracking the given request, clears, and recycles it, and returns {@code true} if the * request was removed or invalid or {@code false} if the request was not found. */ public boolean clearAndRemove(@Nullable Request request) { if (request == null) { // If the Request is null, the request is already cleared and we don't need to search further // for its owner. return true; } boolean isOwnedByUs = requests.remove(request); // Avoid short circuiting. isOwnedByUs = pendingRequests.remove(request) || isOwnedByUs; if (isOwnedByUs) { request.clear(); } return isOwnedByUs; } /** Returns {@code true} if requests are currently paused, and {@code false} otherwise. */ public boolean isPaused() { return isPaused; } /** Stops any in progress requests. */ public void pauseRequests() { isPaused = true; for (Request request : Util.getSnapshot(requests)) { if (request.isRunning()) { // Avoid clearing parts of requests that may have completed (thumbnails) to avoid blinking // in the UI, while still making sure that any in progress parts of requests are immediately // stopped. request.pause(); pendingRequests.add(request); } } } /** Stops any in progress requests and releases bitmaps associated with completed requests. */ public void pauseAllRequests() { isPaused = true; for (Request request : Util.getSnapshot(requests)) { // TODO(judds): Failed requests return false from isComplete(). They're still restarted in // resumeRequests, but they're not cleared here. We should probably clear all requests here? if (request.isRunning() || request.isComplete()) { request.clear(); pendingRequests.add(request); } } } /** Starts any not yet completed or failed requests. */ public void resumeRequests() { isPaused = false; for (Request request : Util.getSnapshot(requests)) { // We don't need to check for cleared here. Any explicit clear by a user will remove the // Request from the tracker, so the only way we'd find a cleared request here is if we cleared // it. As a result it should be safe for us to resume cleared requests. if (!request.isComplete() && !request.isRunning()) { request.begin(); } } pendingRequests.clear(); } /** * Cancels all requests and clears their resources. * *

    After this call requests cannot be restarted. */ public void clearRequests() { for (Request request : Util.getSnapshot(requests)) { // It's unsafe to recycle the Request here because we don't know who might else have a // reference to it. clearAndRemove(request); } pendingRequests.clear(); } /** Restarts failed requests and cancels and restarts in progress requests. */ public void restartRequests() { for (Request request : Util.getSnapshot(requests)) { if (!request.isComplete() && !request.isCleared()) { request.clear(); if (!isPaused) { request.begin(); } else { // Ensure the request will be restarted in onResume. pendingRequests.add(request); } } } } @Override public String toString() { return super.toString() + "{numRequests=" + requests.size() + ", isPaused=" + isPaused + "}"; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/SingletonConnectivityReceiver.java ================================================ package com.bumptech.glide.manager; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Build; import android.os.Build.VERSION_CODES; import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.manager.ConnectivityMonitor.ConnectivityListener; import com.bumptech.glide.util.GlideSuppliers; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; /** Uses {@link android.net.ConnectivityManager} to identify connectivity changes. */ final class SingletonConnectivityReceiver { private static volatile SingletonConnectivityReceiver instance; private static final String TAG = "ConnectivityMonitor"; private final FrameworkConnectivityMonitor frameworkConnectivityMonitor; @GuardedBy("this") @Synthetic final Set listeners = new HashSet(); @GuardedBy("this") private boolean isRegistered; static SingletonConnectivityReceiver get(@NonNull Context context) { if (instance == null) { synchronized (SingletonConnectivityReceiver.class) { if (instance == null) { instance = new SingletonConnectivityReceiver(context.getApplicationContext()); } } } return instance; } @VisibleForTesting static void reset() { instance = null; } private SingletonConnectivityReceiver(final @NonNull Context context) { GlideSupplier connectivityManager = GlideSuppliers.memorize( new GlideSupplier() { @Override public ConnectivityManager get() { return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); } }); ConnectivityListener connectivityListener = new ConnectivityListener() { @Override public void onConnectivityChanged(boolean isConnected) { Util.assertMainThread(); List toNotify; synchronized (SingletonConnectivityReceiver.this) { toNotify = new ArrayList<>(listeners); } for (ConnectivityListener listener : toNotify) { listener.onConnectivityChanged(isConnected); } } }; frameworkConnectivityMonitor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new FrameworkConnectivityMonitorPostApi24(connectivityManager, connectivityListener) : new FrameworkConnectivityMonitorPreApi24( context, connectivityManager, connectivityListener); } synchronized void register(ConnectivityListener listener) { listeners.add(listener); maybeRegisterReceiver(); } /** * To avoid holding a lock while notifying listeners, the unregistered listener may still be * notified about a connectivity change after this method completes if this method is called on a * thread other than the main thread and if a connectivity broadcast is racing with this method. * Callers must handle this case. */ synchronized void unregister(ConnectivityListener listener) { listeners.remove(listener); maybeUnregisterReceiver(); } @GuardedBy("this") private void maybeRegisterReceiver() { if (isRegistered || listeners.isEmpty()) { return; } isRegistered = frameworkConnectivityMonitor.register(); } @GuardedBy("this") private void maybeUnregisterReceiver() { if (!isRegistered || !listeners.isEmpty()) { return; } frameworkConnectivityMonitor.unregister(); isRegistered = false; } private interface FrameworkConnectivityMonitor { boolean register(); void unregister(); } @RequiresApi(VERSION_CODES.N) private static final class FrameworkConnectivityMonitorPostApi24 implements FrameworkConnectivityMonitor { @Synthetic boolean isConnected; @Synthetic final ConnectivityListener listener; private final GlideSupplier connectivityManager; private final NetworkCallback networkCallback = new NetworkCallback() { @Override public void onAvailable(@NonNull Network network) { postOnConnectivityChange(true); } @Override public void onLost(@NonNull Network network) { postOnConnectivityChange(false); } private void postOnConnectivityChange(final boolean newState) { // We could use registerDefaultNetworkCallback with a Handler, but that's only available // on API 26, instead of API 24. We can mimic the same behavior here manually by // posting to the UI thread. All calls have to be posted to make sure that we retain the // original order. Otherwise a call on a background thread, followed by a call on the UI // thread could result in the first call running second. Util.postOnUiThread( new Runnable() { @Override public void run() { onConnectivityChange(newState); } }); } @Synthetic void onConnectivityChange(boolean newState) { // See b/201425456. Util.assertMainThread(); boolean wasConnected = isConnected; isConnected = newState; if (wasConnected != newState) { listener.onConnectivityChanged(newState); } } }; FrameworkConnectivityMonitorPostApi24( GlideSupplier connectivityManager, ConnectivityListener listener) { this.connectivityManager = connectivityManager; this.listener = listener; } // Permissions are checked in the factory instead. @SuppressLint("MissingPermission") @Override public boolean register() { isConnected = connectivityManager.get().getActiveNetwork() != null; try { connectivityManager.get().registerDefaultNetworkCallback(networkCallback); return true; // See b/201664814, b/204226444: At least TooManyRequestsException is not public and // doesn't extend from any subclass :/. } catch (RuntimeException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to register callback", e); } return false; } } @Override public void unregister() { connectivityManager.get().unregisterNetworkCallback(networkCallback); } } /** * All interactions with connectivity manager and registering/unregistering broadcast receivers * are punted to a background thread. We use serial execution to make sure that they still happen * in the correct order. The system calls required to register/unregister the receiver and to * determine connectivity status are expensive to run on the main thread. isConnected and * isRegistered should only be used on the serial background thread. Listeners should only be * notified on the main thread. Because of the delays caused by punting threads, listeners may be * notified with the incorrect state. Howeve the strict ordering means that they will shortly * after be notified with the correct state. */ private static final class FrameworkConnectivityMonitorPreApi24 implements FrameworkConnectivityMonitor { // Using AsyncTasks's executor is a hack. We need a background thread, but it's not trivial to // pass one through to this point. We could make a breaking API change, which upsets external // users. Or we could try to add an API to expose one of Glide's executors via the singleton, // but that could allow Glide's executors to be misused as general purpose executors. Given that // this code is deprecated anyway, using some pre-existing general purpose executor doesn't seem // wildly unreasonable. @Synthetic static final Executor EXECUTOR = AsyncTask.SERIAL_EXECUTOR; @Synthetic final Context context; @Synthetic final ConnectivityListener listener; private final GlideSupplier connectivityManager; // These are only manipulated serially, but the executor might use separate threads to do so, // so we use volatile. @Synthetic volatile boolean isConnected; @Synthetic volatile boolean isRegistered; @Synthetic final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() { @Override public void onReceive(@NonNull Context context, Intent intent) { onConnectivityChange(); } }; FrameworkConnectivityMonitorPreApi24( Context context, GlideSupplier connectivityManager, ConnectivityListener listener) { this.context = context.getApplicationContext(); this.connectivityManager = connectivityManager; this.listener = listener; } @Override public boolean register() { EXECUTOR.execute( new Runnable() { @Override public void run() { // Initialize isConnected so that we notice the first time around when there's a // broadcast. // TODO(judds): This causes a race where: // 1. Connectivity is disconnected // 2. Register is called, but punted to a background thread and not run yet // 3. Some network requiring request is started, runs and fails due to connectivity // 4. Connectivity is re-established // 5. This code finally runs on the background thread. // In step 5, we'll think that we're currently connected and won't trigger a retry for // any previously failed requests. // In the long run it might be nice to define some explicit initialization step for // Glide where we do this and other expensive things on a background thread prior to // starting the first request. For now it seems better to just accept this race than // either take the latency hit of the IPC on the main thread, or try something like // always notifying all listeners once right after this logic runs just in case // something failed. isConnected = isConnected(); try { // See #1405 context.registerReceiver( connectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); isRegistered = true; } catch (SecurityException e) { // See #1417, registering the receiver can throw SecurityException. if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to register", e); } isRegistered = false; } } }); // We track our registration status internally, so we always need to be called to unregister. return true; } @Override public void unregister() { // Always post to the executor to make sure everything runs in the correct order. If we short // circuit that by checking isConnected on this thread, we might leak a receiver. EXECUTOR.execute( new Runnable() { @Override public void run() { if (!isRegistered) { return; } isRegistered = false; context.unregisterReceiver(connectivityReceiver); } }); } @Synthetic void onConnectivityChange() { EXECUTOR.execute( new Runnable() { @Override public void run() { boolean wasConnected = isConnected; isConnected = isConnected(); if (wasConnected != isConnected) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "connectivity changed, isConnected: " + isConnected); } notifyChangeOnUiThread(isConnected); } } }); } // Pass through the boolean because the instance variable could change while we're waiting for // the runnable to be executed on the main thread. @Synthetic void notifyChangeOnUiThread(final boolean isConnected) { Util.postOnUiThread( new Runnable() { @Override public void run() { listener.onConnectivityChanged(isConnected); } }); } @SuppressWarnings("WeakerAccess") @Synthetic // Permissions are checked in the factory instead. @SuppressLint("MissingPermission") boolean isConnected() { NetworkInfo networkInfo; try { networkInfo = connectivityManager.get().getActiveNetworkInfo(); } catch (RuntimeException e) { // #1405 shows that this throws a SecurityException. // b/70869360 shows that this throws NullPointerException on APIs 22, 23, and 24. // b/70869360 also shows that this throws RuntimeException on API 24 and 25. if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to determine connectivity status when connectivity changed", e); } // Default to true; return true; } return networkInfo != null && networkInfo.isConnected(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/SupportRequestManagerFragment.java ================================================ package com.bumptech.glide.manager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.bumptech.glide.RequestManager; /** * A view-less {@link androidx.fragment.app.Fragment} used to safely store an {@link * com.bumptech.glide.RequestManager} that can be used to start, stop and manage Glide requests * started for targets within the fragment or activity this fragment is a child of. * * @see com.bumptech.glide.manager.RequestManagerRetriever * @see com.bumptech.glide.RequestManager * @deprecated This class is unused by Glide. All functionality has been removed. The class will be * removed in a future version. */ @Deprecated public class SupportRequestManagerFragment extends Fragment { /** * @deprecated A no-op method. See the class deprecation method for details. */ @Deprecated public void setRequestManager(@Nullable RequestManager requestManager) {} /** * @deprecated Always returns {@code null}. See the class deprecation method for details. */ @Nullable @Deprecated public RequestManager getRequestManager() { return null; } /** * @deprecated Always returns {@link EmptyRequestManagerTreeNode}. See the class deprecation * method for details. */ @Deprecated @NonNull public RequestManagerTreeNode getRequestManagerTreeNode() { return new EmptyRequestManagerTreeNode(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/manager/TargetTracker.java ================================================ package com.bumptech.glide.manager; import androidx.annotation.NonNull; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.Util; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.WeakHashMap; /** * Holds the set of {@link Target}s currently active for a {@link com.bumptech.glide.RequestManager} * and forwards on lifecycle events. */ public final class TargetTracker implements LifecycleListener { private final Set> targets = Collections.newSetFromMap(new WeakHashMap, Boolean>()); public void track(@NonNull Target target) { targets.add(target); } public void untrack(@NonNull Target target) { targets.remove(target); } @Override public void onStart() { for (Target target : Util.getSnapshot(targets)) { target.onStart(); } } @Override public void onStop() { for (Target target : Util.getSnapshot(targets)) { target.onStop(); } } @Override public void onDestroy() { for (Target target : Util.getSnapshot(targets)) { target.onDestroy(); } } @NonNull public List> getAll() { return Util.getSnapshot(targets); } public void clear() { targets.clear(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/module/AppGlideModule.java ================================================ package com.bumptech.glide.module; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.GlideBuilder; /** * Defines a set of dependencies and options to use when initializing Glide within an application. * *

    There can be at most one {@link AppGlideModule} in an application. Only Applications can * include a {@link AppGlideModule}. Libraries must use {@link LibraryGlideModule}. * *

    Classes that extend {@link AppGlideModule} must be annotated with {@link * com.bumptech.glide.annotation.GlideModule} to be processed correctly. * *

    Classes that extend {@link AppGlideModule} can optionally be annotated with {@link * com.bumptech.glide.annotation.Excludes} to optionally exclude one or more {@link * LibraryGlideModule} and/or {@link GlideModule} classes. * *

    Once an application has migrated itself and all libraries it depends on to use Glide's * annotation processor, {@link AppGlideModule} implementations should override {@link * #isManifestParsingEnabled()} and return {@code false}. */ // Used only in javadoc. @SuppressWarnings("deprecation") public abstract class AppGlideModule extends LibraryGlideModule implements AppliesOptions { /** * Returns {@code true} if Glide should check the AndroidManifest for {@link GlideModule}s. * *

    Implementations should return {@code false} after they and their dependencies have migrated * to Glide's annotation processor. * *

    Returns {@code true} by default. */ public boolean isManifestParsingEnabled() { return true; } @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { // Default empty impl. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/module/AppliesOptions.java ================================================ package com.bumptech.glide.module; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.GlideBuilder; /** An internal interface, to be removed when {@link GlideModule}s are removed. */ @Deprecated interface AppliesOptions { /** * Lazily apply options to a {@link com.bumptech.glide.GlideBuilder} immediately before the Glide * singleton is created. * *

    This method will be called once and only once per implementation. * * @param context An Application {@link android.content.Context}. * @param builder The {@link com.bumptech.glide.GlideBuilder} that will be used to create Glide. */ void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder); } ================================================ FILE: library/src/main/java/com/bumptech/glide/module/GlideModule.java ================================================ package com.bumptech.glide.module; import com.bumptech.glide.Registry; /** * An interface allowing lazy configuration of Glide including setting options using {@link * com.bumptech.glide.GlideBuilder} and registering {@link com.bumptech.glide.load.model.ModelLoader * ModelLoaders}. * *

    To use this interface: * *

      *
    1. Implement the GlideModule interface in a class with public visibility, calling {@link * Registry#prepend(Class, Class, com.bumptech.glide.load.ResourceDecoder)} for each {@link * com.bumptech.glide.load.model.ModelLoader} you'd like to register: *
       *                  
       *                      public class FlickrGlideModule implements GlideModule {
       *                          {@literal @}Override
       *                          public void applyOptions(Context context, GlideBuilder builder) {
       *                              builder.setDecodeFormat(DecodeFormat.ALWAYS_ARGB_8888);
       *                          }
       *
       *                          {@literal @}Override
       *                          public void registerComponents(Context context, Glide glide) {
       *                              glide.register(Model.class, Data.class, new MyModelLoader());
       *                          }
       *                      }
       *                  
       *             
      *
    2. Add your implementation to your list of keeps in your proguard.cfg file: *
      {@code
       * -keepnames class * com.bumptech.glide.samples.flickr.FlickrGlideModule
       * }
      *
    3. Add a metadata tag to your AndroidManifest.xml with your GlideModule implementation's fully * qualified classname as the key, and {@code GlideModule} as the value: *
      {@code
       * 
       * }
      *
    * *

    All implementations must be publicly visible and contain only an empty constructor so they can * be instantiated via reflection when Glide is lazily initialized. * *

    There is no defined order in which modules are called, so projects should be careful to avoid * applying conflicting settings in different modules. If an application depends on libraries that * have conflicting modules, the application should consider avoiding the library modules and * instead providing their required dependencies in a single application module. * * @deprecated Libraries should use {@link LibraryGlideModule} and Applications should use {@link * AppGlideModule}. */ @Deprecated public interface GlideModule extends RegistersComponents, AppliesOptions {} ================================================ FILE: library/src/main/java/com/bumptech/glide/module/LibraryGlideModule.java ================================================ package com.bumptech.glide.module; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; /** * Registers a set of components to use when initializing Glide within an app when Glide's * annotation processor is used. * *

    Any number of LibraryGlideModules can be contained within any library or application. * *

    LibraryGlideModules are called in no defined order. If LibraryGlideModules within an * application conflict, {@link AppGlideModule}s can use the {@link * com.bumptech.glide.annotation.Excludes} annotation to selectively remove one or more of the * conflicting modules. */ @SuppressWarnings("deprecation") public abstract class LibraryGlideModule implements RegistersComponents { @Override public void registerComponents( @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { // Default empty impl. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/module/ManifestParser.java ================================================ package com.bumptech.glide.module; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.util.Log; import androidx.annotation.Nullable; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; /** * Parses {@link com.bumptech.glide.module.GlideModule} references out of the AndroidManifest file. */ // Used only in javadoc. @SuppressWarnings("deprecation") @Deprecated public final class ManifestParser { private static final String TAG = "ManifestParser"; private static final String GLIDE_MODULE_VALUE = "GlideModule"; private final Context context; public ManifestParser(Context context) { this.context = context; } // getApplicationInfo returns null in Compose previews, see #4977 and b/263613353. @SuppressWarnings("ConstantConditions") @Nullable private ApplicationInfo getOurApplicationInfo() throws NameNotFoundException { return context .getPackageManager() .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); } @SuppressWarnings("deprecation") public List parse() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Loading Glide modules"); } List modules = new ArrayList<>(); try { ApplicationInfo appInfo = getOurApplicationInfo(); if (appInfo == null || appInfo.metaData == null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Got null app info metadata"); } return modules; } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Got app info metadata: " + appInfo.metaData); } for (String key : appInfo.metaData.keySet()) { if (GLIDE_MODULE_VALUE.equals(appInfo.metaData.get(key))) { modules.add(parseModule(key)); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Loaded Glide module: " + key); } } } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Finished loading Glide modules"); } } catch (PackageManager.NameNotFoundException e) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Failed to parse glide modules", e); } } return modules; } @SuppressWarnings("deprecation") private static GlideModule parseModule(String className) { Class clazz; try { clazz = Class.forName(className); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Unable to find GlideModule implementation", e); } Object module = null; try { module = clazz.getDeclaredConstructor().newInstance(); // These can't be combined until API minimum is 19. } catch (InstantiationException e) { throwInstantiateGlideModuleException(clazz, e); } catch (IllegalAccessException e) { throwInstantiateGlideModuleException(clazz, e); } catch (NoSuchMethodException e) { throwInstantiateGlideModuleException(clazz, e); } catch (InvocationTargetException e) { throwInstantiateGlideModuleException(clazz, e); } if (!(module instanceof GlideModule)) { throw new RuntimeException("Expected instanceof GlideModule, but found: " + module); } return (GlideModule) module; } private static void throwInstantiateGlideModuleException(Class clazz, Exception e) { throw new RuntimeException("Unable to instantiate GlideModule implementation for " + clazz, e); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/module/RegistersComponents.java ================================================ package com.bumptech.glide.module; import android.content.Context; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; /** An internal interface, to be removed when {@link GlideModule}s are removed. */ // Used only in javadocs. @SuppressWarnings("deprecation") @Deprecated interface RegistersComponents { /** * Lazily register components immediately after the Glide singleton is created but before any * requests can be started. * *

    This method will be called once and only once per implementation. * * @param context An Application {@link android.content.Context}. * @param glide The Glide singleton that is in the process of being initialized. * @param registry An {@link com.bumptech.glide.Registry} to use to register components. */ void registerComponents( @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry); } ================================================ FILE: library/src/main/java/com/bumptech/glide/provider/EncoderRegistry.java ================================================ package com.bumptech.glide.provider; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Encoder; import com.bumptech.glide.util.Synthetic; import java.util.ArrayList; import java.util.List; /** Contains an ordered list of {@link Encoder}s capable of encoding arbitrary data types. */ public class EncoderRegistry { // TODO: This registry should probably contain a put, rather than a list. private final List> encoders = new ArrayList<>(); @SuppressWarnings("unchecked") @Nullable public synchronized Encoder getEncoder(@NonNull Class dataClass) { for (Entry entry : encoders) { if (entry.handles(dataClass)) { return (Encoder) entry.encoder; } } return null; } public synchronized void append(@NonNull Class dataClass, @NonNull Encoder encoder) { encoders.add(new Entry<>(dataClass, encoder)); } public synchronized void prepend(@NonNull Class dataClass, @NonNull Encoder encoder) { encoders.add(0, new Entry<>(dataClass, encoder)); } private static final class Entry { private final Class dataClass; @Synthetic @SuppressWarnings("WeakerAccess") final Encoder encoder; Entry(@NonNull Class dataClass, @NonNull Encoder encoder) { this.dataClass = dataClass; this.encoder = encoder; } boolean handles(@NonNull Class dataClass) { return this.dataClass.isAssignableFrom(dataClass); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/provider/ImageHeaderParserRegistry.java ================================================ package com.bumptech.glide.provider; import androidx.annotation.NonNull; import com.bumptech.glide.load.ImageHeaderParser; import java.util.ArrayList; import java.util.List; /** Contains an unordered list of {@link ImageHeaderParser}s capable of parsing image headers. */ public final class ImageHeaderParserRegistry { private final List parsers = new ArrayList<>(); @NonNull public synchronized List getParsers() { return parsers; } public synchronized void add(@NonNull ImageHeaderParser parser) { parsers.add(parser); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/provider/LoadPathCache.java ================================================ package com.bumptech.glide.provider; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.DecodePath; import com.bumptech.glide.load.engine.LoadPath; import com.bumptech.glide.load.resource.transcode.UnitTranscoder; import com.bumptech.glide.util.MultiClassKey; import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; /** * Maintains a cache of data, resource, and transcode classes to available {@link * com.bumptech.glide.load.engine.LoadPath}s capable of decoding with the requested types. */ public class LoadPathCache { private static final LoadPath NO_PATHS_SIGNAL = new LoadPath<>( Object.class, Object.class, Object.class, Collections.singletonList( new DecodePath<>( Object.class, Object.class, Object.class, Collections.>emptyList(), new UnitTranscoder<>(), /* listPool= */ null)), /* listPool= */ null); private final ArrayMap> cache = new ArrayMap<>(); private final AtomicReference keyRef = new AtomicReference<>(); /** * Returns {@code} true if the given {@link LoadPath} is the signal object returned from {@link * #get(Class, Class, Class)} that indicates that we've previously found that there are no * available paths to load the requested resources and {@code false} otherwise. */ public boolean isEmptyLoadPath(@Nullable LoadPath path) { return NO_PATHS_SIGNAL.equals(path); } /** * May return {@link #NO_PATHS_SIGNAL} to indicate that we've previously found that there are 0 * available load paths for the requested types. Callers must check using {@link * #isEmptyLoadPath(LoadPath)} before using any load path returned by this method. */ @SuppressWarnings("unchecked") @Nullable public LoadPath get( Class dataClass, Class resourceClass, Class transcodeClass) { MultiClassKey key = getKey(dataClass, resourceClass, transcodeClass); LoadPath result; synchronized (cache) { result = cache.get(key); } keyRef.set(key); return (LoadPath) result; } public void put( Class dataClass, Class resourceClass, Class transcodeClass, @Nullable LoadPath loadPath) { synchronized (cache) { cache.put( new MultiClassKey(dataClass, resourceClass, transcodeClass), loadPath != null ? loadPath : NO_PATHS_SIGNAL); } } private MultiClassKey getKey( Class dataClass, Class resourceClass, Class transcodeClass) { MultiClassKey key = keyRef.getAndSet(null); if (key == null) { key = new MultiClassKey(); } key.set(dataClass, resourceClass, transcodeClass); return key; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/provider/ModelToResourceClassCache.java ================================================ package com.bumptech.glide.provider; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import com.bumptech.glide.util.MultiClassKey; import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** * Maintains a cache of Model + Resource class to a set of registered resource classes that are * subclasses of the resource class that can be decoded from the model class. */ public class ModelToResourceClassCache { private final AtomicReference resourceClassKeyRef = new AtomicReference<>(); private final ArrayMap>> registeredResourceClassCache = new ArrayMap<>(); @Nullable public List> get( @NonNull Class modelClass, @NonNull Class resourceClass, @NonNull Class transcodeClass) { MultiClassKey key = resourceClassKeyRef.getAndSet(null); if (key == null) { key = new MultiClassKey(modelClass, resourceClass, transcodeClass); } else { key.set(modelClass, resourceClass, transcodeClass); } final List> result; synchronized (registeredResourceClassCache) { result = registeredResourceClassCache.get(key); } resourceClassKeyRef.set(key); return result; } public void put( @NonNull Class modelClass, @NonNull Class resourceClass, @NonNull Class transcodeClass, @NonNull List> resourceClasses) { synchronized (registeredResourceClassCache) { registeredResourceClassCache.put( new MultiClassKey(modelClass, resourceClass, transcodeClass), resourceClasses); } } public void clear() { synchronized (registeredResourceClassCache) { registeredResourceClassCache.clear(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/provider/ResourceDecoderRegistry.java ================================================ package com.bumptech.glide.provider; import androidx.annotation.NonNull; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.util.Synthetic; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Contains an ordered list of {@link ResourceDecoder}s capable of decoding arbitrary data types * into arbitrary resource types from highest priority decoders to lowest priority decoders. */ @SuppressWarnings("rawtypes") public class ResourceDecoderRegistry { private final List bucketPriorityList = new ArrayList<>(); private final Map>> decoders = new HashMap<>(); public synchronized void setBucketPriorityList(@NonNull List buckets) { List previousBuckets = new ArrayList<>(bucketPriorityList); bucketPriorityList.clear(); // new ArrayList(List) and ArrayList#addAll(List) are both broken on some verisons of Android, // see #3296 for (String bucket : buckets) { bucketPriorityList.add(bucket); } for (String previousBucket : previousBuckets) { if (!buckets.contains(previousBucket)) { // Keep any buckets from the previous list that aren't included here, but but them at the // end. bucketPriorityList.add(previousBucket); } } } @NonNull @SuppressWarnings("unchecked") public synchronized List> getDecoders( @NonNull Class dataClass, @NonNull Class resourceClass) { List> result = new ArrayList<>(); for (String bucket : bucketPriorityList) { List> entries = decoders.get(bucket); if (entries == null) { continue; } for (Entry entry : entries) { if (entry.handles(dataClass, resourceClass)) { result.add((ResourceDecoder) entry.decoder); } } } // TODO: cache result list. return result; } @NonNull @SuppressWarnings("unchecked") public synchronized List> getResourceClasses( @NonNull Class dataClass, @NonNull Class resourceClass) { List> result = new ArrayList<>(); for (String bucket : bucketPriorityList) { List> entries = decoders.get(bucket); if (entries == null) { continue; } for (Entry entry : entries) { if (entry.handles(dataClass, resourceClass) && !result.contains((Class) entry.resourceClass)) { result.add((Class) entry.resourceClass); } } } return result; } public synchronized void append( @NonNull String bucket, @NonNull ResourceDecoder decoder, @NonNull Class dataClass, @NonNull Class resourceClass) { getOrAddEntryList(bucket).add(new Entry<>(dataClass, resourceClass, decoder)); } public synchronized void prepend( @NonNull String bucket, @NonNull ResourceDecoder decoder, @NonNull Class dataClass, @NonNull Class resourceClass) { getOrAddEntryList(bucket).add(0, new Entry<>(dataClass, resourceClass, decoder)); } @NonNull private synchronized List> getOrAddEntryList(@NonNull String bucket) { if (!bucketPriorityList.contains(bucket)) { // Add this unspecified bucket as a low priority bucket. bucketPriorityList.add(bucket); } List> entries = decoders.get(bucket); if (entries == null) { entries = new ArrayList<>(); decoders.put(bucket, entries); } return entries; } private static class Entry { private final Class dataClass; @Synthetic final Class resourceClass; @Synthetic final ResourceDecoder decoder; public Entry( @NonNull Class dataClass, @NonNull Class resourceClass, ResourceDecoder decoder) { this.dataClass = dataClass; this.resourceClass = resourceClass; this.decoder = decoder; } public boolean handles(@NonNull Class dataClass, @NonNull Class resourceClass) { return this.dataClass.isAssignableFrom(dataClass) && resourceClass.isAssignableFrom(this.resourceClass); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/provider/ResourceEncoderRegistry.java ================================================ package com.bumptech.glide.provider; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.ResourceEncoder; import com.bumptech.glide.util.Synthetic; import java.util.ArrayList; import java.util.List; /** * Contains an ordered list of {@link ResourceEncoder}s capable of encoding arbitrary resource * types. */ public class ResourceEncoderRegistry { // TODO: this should probably be a put. private final List> encoders = new ArrayList<>(); public synchronized void append( @NonNull Class resourceClass, @NonNull ResourceEncoder encoder) { encoders.add(new Entry<>(resourceClass, encoder)); } public synchronized void prepend( @NonNull Class resourceClass, @NonNull ResourceEncoder encoder) { encoders.add(0, new Entry<>(resourceClass, encoder)); } @SuppressWarnings("unchecked") @Nullable public synchronized ResourceEncoder get(@NonNull Class resourceClass) { //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = encoders.size(); i < size; i++) { Entry entry = encoders.get(i); if (entry.handles(resourceClass)) { return (ResourceEncoder) entry.encoder; } } // TODO: throw an exception here? return null; } private static final class Entry { private final Class resourceClass; @Synthetic final ResourceEncoder encoder; Entry(@NonNull Class resourceClass, @NonNull ResourceEncoder encoder) { this.resourceClass = resourceClass; this.encoder = encoder; } @Synthetic boolean handles(@NonNull Class resourceClass) { return this.resourceClass.isAssignableFrom(resourceClass); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/BaseRequestOptions.java ================================================ package com.bumptech.glide.request; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.MultiTransformation; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.model.stream.HttpGlideUrlLoader; import com.bumptech.glide.load.resource.bitmap.BitmapEncoder; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.CenterInside; import com.bumptech.glide.load.resource.bitmap.CircleCrop; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.load.resource.bitmap.DrawableTransformation; import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.VideoDecoder; import com.bumptech.glide.load.resource.drawable.ResourceDrawableDecoder; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.load.resource.gif.GifDrawableTransformation; import com.bumptech.glide.load.resource.gif.GifOptions; import com.bumptech.glide.signature.EmptySignature; import com.bumptech.glide.util.CachedHashCodeArrayMap; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; import java.util.Map; /** * A base object to allow method sharing between {@link RequestOptions} and {@link * com.bumptech.glide.RequestBuilder}. * *

    This class is not meant for general use and may change at any time. * * @param The particular child implementation */ @SuppressWarnings({"PMD.UseUtilityClass", "unused"}) public abstract class BaseRequestOptions> implements Cloneable { private static final int UNSET = -1; private static final int SIZE_MULTIPLIER = 1 << 1; private static final int DISK_CACHE_STRATEGY = 1 << 2; private static final int PRIORITY = 1 << 3; private static final int ERROR_PLACEHOLDER = 1 << 4; private static final int ERROR_ID = 1 << 5; private static final int PLACEHOLDER = 1 << 6; private static final int PLACEHOLDER_ID = 1 << 7; private static final int IS_CACHEABLE = 1 << 8; private static final int OVERRIDE = 1 << 9; private static final int SIGNATURE = 1 << 10; private static final int TRANSFORMATION = 1 << 11; private static final int RESOURCE_CLASS = 1 << 12; private static final int FALLBACK = 1 << 13; private static final int FALLBACK_ID = 1 << 14; private static final int THEME = 1 << 15; private static final int TRANSFORMATION_ALLOWED = 1 << 16; private static final int TRANSFORMATION_REQUIRED = 1 << 17; private static final int USE_UNLIMITED_SOURCE_GENERATORS_POOL = 1 << 18; private static final int ONLY_RETRIEVE_FROM_CACHE = 1 << 19; private static final int USE_ANIMATION_POOL = 1 << 20; private int fields; private float sizeMultiplier = 1f; @NonNull private DiskCacheStrategy diskCacheStrategy = DiskCacheStrategy.AUTOMATIC; @NonNull private Priority priority = Priority.NORMAL; @Nullable private Drawable errorPlaceholder; private int errorId; @Nullable private Drawable placeholderDrawable; private int placeholderId; private boolean isCacheable = true; private int overrideHeight = UNSET; private int overrideWidth = UNSET; @NonNull private Key signature = EmptySignature.obtain(); private boolean isTransformationRequired; private boolean isTransformationAllowed = true; @Nullable private Drawable fallbackDrawable; private int fallbackId; @NonNull private Options options = new Options(); @NonNull private Map, Transformation> transformations = new CachedHashCodeArrayMap<>(); @NonNull private Class resourceClass = Object.class; private boolean isLocked; @Nullable private Resources.Theme theme; private boolean isAutoCloneEnabled; private boolean useUnlimitedSourceGeneratorsPool; private boolean onlyRetrieveFromCache; private boolean isScaleOnlyOrNoTransform = true; private boolean useAnimationPool; private static boolean isSet(int fields, int flag) { return (fields & flag) != 0; } /** * Applies a multiplier to the {@link com.bumptech.glide.request.target.Target}'s size before * loading the resource. Useful for loading thumbnails or trying to avoid loading huge resources * (particularly {@link Bitmap}s on devices with overly dense screens. * * @param sizeMultiplier The multiplier to apply to the {@link * com.bumptech.glide.request.target.Target}'s dimensions when loading the resource. * @return This request builder. */ @NonNull @CheckResult public T sizeMultiplier(@FloatRange(from = 0, to = 1) float sizeMultiplier) { if (isAutoCloneEnabled) { return clone().sizeMultiplier(sizeMultiplier); } if (sizeMultiplier < 0f || sizeMultiplier > 1f) { throw new IllegalArgumentException("sizeMultiplier must be between 0 and 1"); } this.sizeMultiplier = sizeMultiplier; fields |= SIZE_MULTIPLIER; return selfOrThrowIfLocked(); } /** * If set to {@code true}, uses a cached unlimited {@link java.util.concurrent.Executor} to run * the request. * *

    This method should ONLY be used when a Glide load is started recursively on one of * Glide's threads as part of another request. Using this method in other scenarios can lead to * excessive memory usage and OOMs and/or a significant decrease in performance across an * application. * *

    If both this method and {@link #useAnimationPool(boolean)} are set, this method will be * preferred and {@link #useAnimationPool(boolean)} will be ignored. */ @NonNull @CheckResult public T useUnlimitedSourceGeneratorsPool(boolean flag) { if (isAutoCloneEnabled) { return clone().useUnlimitedSourceGeneratorsPool(flag); } this.useUnlimitedSourceGeneratorsPool = flag; fields |= USE_UNLIMITED_SOURCE_GENERATORS_POOL; return selfOrThrowIfLocked(); } /** * If set to {@code true}, uses a special {@link java.util.concurrent.Executor} that is used * exclusively for decoding frames of animated resources, like GIFs. * *

    The animation executor disallows network operations and must not be used for loads that may * load remote data. The animation executor has fewer threads available to it than Glide's normal * executors and is only useful as a way of avoiding blocking on longer and more expensive reads * for critical requests like those in an animating GIF. * *

    If both {@link #useUnlimitedSourceGeneratorsPool(boolean)} and this method are set, {@link * #useUnlimitedSourceGeneratorsPool(boolean)} will be preferred and this method will be ignored. */ @NonNull @CheckResult public T useAnimationPool(boolean flag) { if (isAutoCloneEnabled) { return clone().useAnimationPool(flag); } useAnimationPool = flag; fields |= USE_ANIMATION_POOL; return selfOrThrowIfLocked(); } /** * If set to true, will only load an item if found in the cache, and will not fetch from source. * *

    By 'cache' we mean both the in memory cache and both types of disk cache ({@link * DiskCacheStrategy#DATA} and {@link DiskCacheStrategy#RESOURCE}). If this flag is set to {@code * true} and the item is not in the memory cache, but it is in one of the disk caches, the load * will complete asynchronously. * *

    If you'd like to only load an item from the memory cache. You can call this method with * {@code true} and also call {@link #diskCacheStrategy(DiskCacheStrategy)} with {@link * DiskCacheStrategy#NONE} */ @NonNull @CheckResult public T onlyRetrieveFromCache(boolean flag) { if (isAutoCloneEnabled) { return clone().onlyRetrieveFromCache(flag); } this.onlyRetrieveFromCache = flag; fields |= ONLY_RETRIEVE_FROM_CACHE; return selfOrThrowIfLocked(); } /** * Sets the {@link DiskCacheStrategy} to use for this load. * *

    Defaults to {@link DiskCacheStrategy#AUTOMATIC}. * *

    For most applications {@link DiskCacheStrategy#RESOURCE} is ideal. Applications that use the * same resource multiple times in multiple sizes and are willing to trade off some speed and disk * space in return for lower bandwidth usage may want to consider using {@link * DiskCacheStrategy#DATA} or {@link DiskCacheStrategy#ALL}. * * @param strategy The strategy to use. * @return This request builder. */ @NonNull @CheckResult public T diskCacheStrategy(@NonNull DiskCacheStrategy strategy) { if (isAutoCloneEnabled) { return clone().diskCacheStrategy(strategy); } this.diskCacheStrategy = Preconditions.checkNotNull(strategy); fields |= DISK_CACHE_STRATEGY; return selfOrThrowIfLocked(); } /** * Sets the priority for this load. * * @param priority A priority. * @return This request builder. */ @NonNull @CheckResult public T priority(@NonNull Priority priority) { if (isAutoCloneEnabled) { return clone().priority(priority); } this.priority = Preconditions.checkNotNull(priority); fields |= PRIORITY; return selfOrThrowIfLocked(); } /** * Sets an {@link Drawable} to display while a resource is loading. * *

    Replaces any previous calls to this method or {@link #placeholder(int)}. * * @param drawable The drawable to display as a placeholder. * @return This request builder. */ @NonNull @CheckResult public T placeholder(@Nullable Drawable drawable) { if (isAutoCloneEnabled) { return clone().placeholder(drawable); } this.placeholderDrawable = drawable; fields |= PLACEHOLDER; placeholderId = 0; fields &= ~PLACEHOLDER_ID; return selfOrThrowIfLocked(); } /** * Sets an Android resource id for a {@link Drawable} resource to display while a resource is * loading. * *

    Replaces any previous calls to this method or {@link #placeholder(Drawable)} * * @param resourceId The id of the resource to use as a placeholder * @return This request builder. */ @NonNull @CheckResult public T placeholder(@DrawableRes int resourceId) { if (isAutoCloneEnabled) { return clone().placeholder(resourceId); } this.placeholderId = resourceId; fields |= PLACEHOLDER_ID; placeholderDrawable = null; fields &= ~PLACEHOLDER; return selfOrThrowIfLocked(); } /** * Sets an {@link Drawable} to display if the model provided to {@link * com.bumptech.glide.RequestBuilder#load(Object)} is {@code null}. * *

    If a fallback is not set, null models will cause the error drawable to be displayed. If the * error drawable is not set, the placeholder will be displayed. * *

    Replaces any previous calls to this method or {@link #fallback(int)}. * * @see #placeholder(Drawable) * @see #placeholder(int) * @param drawable The drawable to display as a placeholder. * @return This request builder. */ @NonNull @CheckResult public T fallback(@Nullable Drawable drawable) { if (isAutoCloneEnabled) { return clone().fallback(drawable); } this.fallbackDrawable = drawable; fields |= FALLBACK; fallbackId = 0; fields &= ~FALLBACK_ID; return selfOrThrowIfLocked(); } /** * Sets a resource to display if the model provided to {@link * com.bumptech.glide.RequestBuilder#load(Object)} is {@code null}. * *

    If a fallback is not set, null models will cause the error drawable to be displayed. If the * error drawable is not set, the placeholder will be displayed. * *

    Replaces any previous calls to this method or {@link #fallback(Drawable)}. * * @see #placeholder(Drawable) * @see #placeholder(int) * @param resourceId The id of the resource to use as a fallback. * @return This request builder. */ @NonNull @CheckResult public T fallback(@DrawableRes int resourceId) { if (isAutoCloneEnabled) { return clone().fallback(resourceId); } this.fallbackId = resourceId; fields |= FALLBACK_ID; fallbackDrawable = null; fields &= ~FALLBACK; return selfOrThrowIfLocked(); } /** * Sets a {@link Drawable} to display if a load fails. * *

    Replaces any previous calls to this method or {@link #error(int)} * * @param drawable The drawable to display. * @return This request builder. */ @NonNull @CheckResult public T error(@Nullable Drawable drawable) { if (isAutoCloneEnabled) { return clone().error(drawable); } this.errorPlaceholder = drawable; fields |= ERROR_PLACEHOLDER; this.errorId = 0; fields &= ~ERROR_ID; return selfOrThrowIfLocked(); } /** * Sets a resource to display if a load fails. * *

    Replaces any previous calls to this method or {@link #error(Drawable)} * * @param resourceId The id of the resource to use as a placeholder. * @return This request builder. */ @NonNull @CheckResult public T error(@DrawableRes int resourceId) { if (isAutoCloneEnabled) { return clone().error(resourceId); } this.errorId = resourceId; fields |= ERROR_ID; this.errorPlaceholder = null; fields &= ~ERROR_PLACEHOLDER; return selfOrThrowIfLocked(); } /** * Sets the {@link android.content.res.Resources.Theme} to apply when loading {@link Drawable}s * for resource ids, including those provided via {@link #error(int)}, {@link #placeholder(int)}, * and {@link #fallback(Drawable)}. * *

    The {@link android.content.res.Resources.Theme} provided here will override the {@link * android.content.res.Resources.Theme} of the application {@link android.content.Context}. * * @param theme The theme to use when loading Drawables. * @return this request builder. */ @NonNull @CheckResult public T theme(@Nullable Resources.Theme theme) { if (isAutoCloneEnabled) { return clone().theme(theme); } this.theme = theme; if (theme != null) { fields |= THEME; return set(ResourceDrawableDecoder.THEME, theme); } else { fields &= ~THEME; return removeOption(ResourceDrawableDecoder.THEME); } } /** * Allows the loaded resource to skip the memory cache. * *

    Note - this is not a guarantee. If a request is already pending for this resource and that * request is not also skipping the memory cache, the resource will be cached in memory. * * @param skip True to allow the resource to skip the memory cache. * @return This request builder. */ @NonNull @CheckResult public T skipMemoryCache(boolean skip) { if (isAutoCloneEnabled) { return clone().skipMemoryCache(true); } this.isCacheable = !skip; fields |= IS_CACHEABLE; return selfOrThrowIfLocked(); } /** * Overrides the {@link com.bumptech.glide.request.target.Target}'s width and height with the * given values. This is useful for thumbnails, and should only be used for other cases when you * need a very specific image size. * * @param width The width in pixels to use to load the resource. * @param height The height in pixels to use to load the resource. * @return This request builder. */ @NonNull @CheckResult public T override(int width, int height) { if (isAutoCloneEnabled) { return clone().override(width, height); } this.overrideWidth = width; this.overrideHeight = height; fields |= OVERRIDE; return selfOrThrowIfLocked(); } /** * Overrides the {@link com.bumptech.glide.request.target.Target}'s width and height with the * given size. * * @see #override(int, int) * @param size The width and height to use. * @return This request builder. */ @NonNull @CheckResult public T override(int size) { return override(size, size); } /** * Sets some additional data to be mixed in to the memory and disk cache keys allowing the caller * more control over when cached data is invalidated. * *

    Note - The signature does not replace the cache key, it is purely additive. * * @param signature A unique non-null {@link Key} representing the current state of the model that * will be mixed in to the cache key. * @return This request builder. * @see com.bumptech.glide.signature.ObjectKey */ @NonNull @CheckResult public T signature(@NonNull Key signature) { if (isAutoCloneEnabled) { return clone().signature(signature); } this.signature = Preconditions.checkNotNull(signature); fields |= SIGNATURE; return selfOrThrowIfLocked(); } /** * Returns a copy of this request builder with all of the options put so far on this builder. * *

    This method returns a "deep" copy in that all non-immutable arguments are copied such that * changes to one builder will not affect the other builder. However, in addition to immutable * arguments, the current model is not copied copied so changes to the model will affect both * builders. * *

    Even if this object was locked, the cloned object returned from this method will not be * locked. */ @SuppressWarnings({ "unchecked", // we don't want to throw to be user friendly "PMD.CloneThrowsCloneNotSupportedException", // The types we're using here do this automatically. "PMD.CloneMethodReturnTypeMustMatchClassName" }) @CheckResult @Override public T clone() { try { BaseRequestOptions result = (BaseRequestOptions) super.clone(); result.options = new Options(); result.options.putAll(options); result.transformations = new CachedHashCodeArrayMap<>(); result.transformations.putAll(transformations); result.isLocked = false; result.isAutoCloneEnabled = false; return (T) result; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } @NonNull @CheckResult public T set(@NonNull Option option, @NonNull Y value) { if (isAutoCloneEnabled) { return clone().set(option, value); } Preconditions.checkNotNull(option); Preconditions.checkNotNull(value); options.set(option, value); return selfOrThrowIfLocked(); } T removeOption(@NonNull Option option) { if (isAutoCloneEnabled) { return clone().removeOption(option); } options.remove(option); return selfOrThrowIfLocked(); } @NonNull @CheckResult public T decode(@NonNull Class resourceClass) { if (isAutoCloneEnabled) { return clone().decode(resourceClass); } this.resourceClass = Preconditions.checkNotNull(resourceClass); fields |= RESOURCE_CLASS; return selfOrThrowIfLocked(); } public final boolean isTransformationAllowed() { return isTransformationAllowed; } public final boolean isTransformationSet() { return isSet(TRANSFORMATION); } public final boolean isLocked() { return isLocked; } /** * Sets the value for key {@link * com.bumptech.glide.load.resource.bitmap.BitmapEncoder#COMPRESSION_FORMAT}. */ @NonNull @CheckResult public T encodeFormat(@NonNull Bitmap.CompressFormat format) { return set(BitmapEncoder.COMPRESSION_FORMAT, Preconditions.checkNotNull(format)); } /** Sets the value for key {@link BitmapEncoder#COMPRESSION_QUALITY}. */ @NonNull @CheckResult public T encodeQuality(@IntRange(from = 0, to = 100) int quality) { return set(BitmapEncoder.COMPRESSION_QUALITY, quality); } /** * Sets the time position of the frame to extract from a video. * *

    This is a component option specific to {@link VideoDecoder}. If the default video decoder is * replaced or skipped because of your configuration, this option may be ignored. * * @see VideoDecoder#TARGET_FRAME * @param frameTimeMicros The time position in microseconds of the desired frame. If negative, the * Android framework implementation return a representative frame. */ @NonNull @CheckResult public T frame(@IntRange(from = 0) long frameTimeMicros) { return set(VideoDecoder.TARGET_FRAME, frameTimeMicros); } /** * Sets the {@link DecodeFormat} to use when decoding {@link Bitmap} objects using {@link * Downsampler} and Glide's default GIF decoders. * *

    {@link DecodeFormat} is a request, not a requirement. It's possible the resource will be * decoded using a decoder that cannot control the format ({@link * android.media.MediaMetadataRetriever} for example), or that the decoder may choose to ignore * the requested format if it can't display the image (i.e. RGB_565 is requested, but the image * has alpha). * *

    This is a component option specific to {@link Downsampler} and Glide's GIF decoders. If the * default Bitmap decoders are replaced or skipped because of your configuration, this option may * be ignored. * *

    To set only the format used when decoding {@link Bitmap}s, use {@link #set(Option, Object)}} * and {@link Downsampler#DECODE_FORMAT}. To set only the format used when decoding GIF frames, * use {@link #set(Option, Object)} and {@link GifOptions#DECODE_FORMAT}. * * @see Downsampler#DECODE_FORMAT * @see GifOptions#DECODE_FORMAT */ @NonNull @CheckResult public T format(@NonNull DecodeFormat format) { Preconditions.checkNotNull(format); return set(Downsampler.DECODE_FORMAT, format).set(GifOptions.DECODE_FORMAT, format); } /** * Disables the use of {@link android.graphics.Bitmap.Config#HARDWARE} in {@link Downsampler} to * avoid errors caused by inspecting Bitmap pixels, drawing with hardware support disabled, * drawing to {@link android.graphics.Canvas}s backed by {@link Bitmap}s etc. * *

    It's almost never safe to set {@link Downsampler#ALLOW_HARDWARE_CONFIG} to {@code true} so * we only provide a way to disable hardware configs entirely. If no option is set for {@link * Downsampler#ALLOW_HARDWARE_CONFIG}, Glide will set the value per request based on whether or * not a {@link Transformation} is applied and if one is, the type of {@link Transformation} * applied. Built in transformations like {@link FitCenter} and {@link * com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.CenterOutside} can safely use {@link * android.graphics.Bitmap.Config#HARDWARE} because they can be entirely replaced by scaling * within {@link Downsampler}. {@link Transformation}s like {@link #circleCrop()} that can't be * replicated by {@link Downsampler} cannot use {@link Bitmap.Config#HARDWARE} because {@link * android.graphics.Bitmap.Config#HARDWARE} cannot be drawn to {@link android.graphics.Canvas}s, * which is required by most {@link Transformation}s. */ @NonNull @CheckResult public T disallowHardwareConfig() { return set(Downsampler.ALLOW_HARDWARE_CONFIG, false); } /** * Sets the {@link DownsampleStrategy} to use when decoding {@link Bitmap Bitmaps} using {@link * Downsampler}. * *

    This is a component option specific to {@link Downsampler}. If the defautlt Bitmap decoder * is replaced or skipped because of your configuration, this option may be ignored. */ @NonNull @CheckResult public T downsample(@NonNull DownsampleStrategy strategy) { return set(DownsampleStrategy.OPTION, Preconditions.checkNotNull(strategy)); } /** * Sets the read and write timeout for the http requests used to load the image. * *

    This is a component option specific to Glide's default networking library and {@link * com.bumptech.glide.load.model.stream.HttpGlideUrlLoader}. If you use any other networking * library including Glide's Volley or OkHttp integration libraries, this option will be ignored. * * @see com.bumptech.glide.load.model.stream.HttpGlideUrlLoader#TIMEOUT * @param timeoutMs The read and write timeout in milliseconds. */ @NonNull @CheckResult public T timeout(@IntRange(from = 0) int timeoutMs) { return set(HttpGlideUrlLoader.TIMEOUT, timeoutMs); } /** * Applies {@link com.bumptech.glide.load.resource.bitmap.CenterCrop} to all default types, and * ignores unknown types. * *

    This will override previous calls to {@link #dontTransform()}. * * @see #optionalTransform(Class, Transformation) * @see #centerCrop() */ @NonNull @CheckResult public T optionalCenterCrop() { return optionalTransform(DownsampleStrategy.CENTER_OUTSIDE, new CenterCrop()); } /** * Applies {@link CenterCrop} to all default types and throws an exception if asked to transform * an unknown type. * *

    this will override previous calls to {@link #dontTransform()} ()}. * * @see #transform(Class, Transformation) * @see #optionalCenterCrop() */ @NonNull @CheckResult public T centerCrop() { return transform(DownsampleStrategy.CENTER_OUTSIDE, new CenterCrop()); } /** * Applies {@link FitCenter} and to all default types, {@link DownsampleStrategy#FIT_CENTER} to * image types, and ignores unknown types. * *

    This will override previous calls to {@link #dontTransform()} and previous calls to {@link * #downsample(DownsampleStrategy)}. * * @see #optionalTransform(Class, Transformation) * @see #fitCenter() */ @NonNull @CheckResult public T optionalFitCenter() { return optionalScaleOnlyTransform(DownsampleStrategy.FIT_CENTER, new FitCenter()); } /** * Applies {@link FitCenter} and to all default types, {@link DownsampleStrategy#FIT_CENTER} to * image types, and throws an exception if asked to transform an unknown type. * *

    This will override previous calls to {@link #dontTransform()} and previous calls to {@link * #downsample(DownsampleStrategy)}. * * @see #transform(Class, Transformation) * @see #optionalFitCenter() */ @NonNull @CheckResult public T fitCenter() { return scaleOnlyTransform(DownsampleStrategy.FIT_CENTER, new FitCenter()); } /** * Applies {@link com.bumptech.glide.load.resource.bitmap.CenterInside} to all default types, * {@link DownsampleStrategy#CENTER_INSIDE} to image types, and ignores unknown types. * *

    This will override previous calls to {@link #dontTransform()} and previous calls to {@link * #downsample(DownsampleStrategy)}. * * @see #optionalTransform(Class, Transformation) * @see #centerInside() */ @NonNull @CheckResult public T optionalCenterInside() { return optionalScaleOnlyTransform(DownsampleStrategy.CENTER_INSIDE, new CenterInside()); } /** * Applies {@link CenterInside} to all default types, {@link DownsampleStrategy#CENTER_INSIDE} to * image types and throws an exception if asked to transform an unknown type. * *

    This will override previous calls to {@link #dontTransform()} and previous calls to {@link * #downsample(DownsampleStrategy)}. * * @see #transform(Class, Transformation) * @see #optionalCenterInside() */ @NonNull @CheckResult public T centerInside() { return scaleOnlyTransform(DownsampleStrategy.CENTER_INSIDE, new CenterInside()); } /** * Applies {@link CircleCrop} to all default types, and ignores unknown types. * *

    This will override previous calls to {@link #dontTransform()}. * * @see #optionalTransform(Transformation) * @see #circleCrop() */ @NonNull @CheckResult public T optionalCircleCrop() { return optionalTransform(DownsampleStrategy.CENTER_OUTSIDE, new CircleCrop()); } /** * Applies {@link CircleCrop} to all default types and throws an exception if asked to transform * an unknown type. * *

    This will override previous calls to {@link #dontTransform()}. * * @see #transform(Class, Transformation) * @see #optionalCenterCrop() */ @NonNull @CheckResult public T circleCrop() { return transform(DownsampleStrategy.CENTER_INSIDE, new CircleCrop()); } // calling optionalTransform() on the result of clone() requires greater access. // calling downsample is guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings({"WeakerAccess", "CheckResult"}) @NonNull final T optionalTransform( @NonNull DownsampleStrategy downsampleStrategy, @NonNull Transformation transformation) { if (isAutoCloneEnabled) { return clone().optionalTransform(downsampleStrategy, transformation); } downsample(downsampleStrategy); return transform(transformation, /* isRequired= */ false); } // calling transform() on the result of clone() requires greater access. // calling downsample is guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings({"WeakerAccess", "CheckResult"}) @NonNull @CheckResult final T transform( @NonNull DownsampleStrategy downsampleStrategy, @NonNull Transformation transformation) { if (isAutoCloneEnabled) { return clone().transform(downsampleStrategy, transformation); } downsample(downsampleStrategy); return transform(transformation); } @NonNull private T scaleOnlyTransform( @NonNull DownsampleStrategy strategy, @NonNull Transformation transformation) { return scaleOnlyTransform(strategy, transformation, true /*isTransformationRequired*/); } @NonNull private T optionalScaleOnlyTransform( @NonNull DownsampleStrategy strategy, @NonNull Transformation transformation) { return scaleOnlyTransform(strategy, transformation, false /*isTransformationRequired*/); } // We know that result will always be T since we created result. @SuppressWarnings("unchecked") @NonNull private T scaleOnlyTransform( @NonNull DownsampleStrategy strategy, @NonNull Transformation transformation, boolean isTransformationRequired) { BaseRequestOptions result = isTransformationRequired ? transform(strategy, transformation) : optionalTransform(strategy, transformation); result.isScaleOnlyOrNoTransform = true; return (T) result; } /** * Applies the given {@link Transformation} for {@link Bitmap Bitmaps} to the default types * ({@link Bitmap}, {@link android.graphics.drawable.BitmapDrawable}, and {@link * com.bumptech.glide.load.resource.gif.GifDrawable}) and throws an exception if asked to * transform an unknown type. * *

    This will override previous calls to {@link #dontTransform()}. * * @param transformation Any {@link Transformation} for {@link Bitmap}s. * @see #optionalTransform(Transformation) * @see #optionalTransform(Class, Transformation) */ // Guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings("CheckResult") @NonNull @CheckResult public T transform(@NonNull Transformation transformation) { return transform(transformation, /* isRequired= */ true); } /** * Applies the given {@link Transformation}s in the given order for {@link Bitmap Bitmaps} to the * default types ({@link Bitmap}, {@link android.graphics.drawable.BitmapDrawable}, and {@link * com.bumptech.glide.load.resource.gif.GifDrawable}) and throws an exception if asked to * transform an unknown type. * *

    This will override previous calls to {@link #dontTransform()}. * * @param transformations One or more {@link Transformation}s for {@link Bitmap}s. * @see #optionalTransform(Transformation) * @see #optionalTransform(Class, Transformation) */ // Guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings({"unchecked", "varargs", "CheckResult"}) @NonNull @CheckResult public T transform(@NonNull Transformation... transformations) { if (transformations.length > 1) { return transform(new MultiTransformation<>(transformations), /* isRequired= */ true); } else if (transformations.length == 1) { return transform(transformations[0]); } else { return selfOrThrowIfLocked(); } } /** * Applies the given {@link Transformation}s in the given order for {@link Bitmap Bitmaps} to the * default types ({@link Bitmap}, {@link android.graphics.drawable.BitmapDrawable}, and {@link * com.bumptech.glide.load.resource.gif.GifDrawable}) and throws an exception if asked to * transform an unknown type. * *

    This will override previous calls to {@link #dontTransform()}. * * @deprecated Deprecated due to api update, use {@link #transform(Transformation[])} instead * @param transformations One or more {@link Transformation}s for {@link Bitmap}s. * @see #optionalTransform(Transformation) * @see #optionalTransform(Class, Transformation) */ // Guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings({"unchecked", "varargs", "CheckResult"}) @NonNull @CheckResult @Deprecated public T transforms(@NonNull Transformation... transformations) { return transform(new MultiTransformation<>(transformations), /* isRequired= */ true); } /** * Applies the given {@link Transformation} for {@link Bitmap Bitmaps} to the default types * ({@link Bitmap}, {@link android.graphics.drawable.BitmapDrawable}, and {@link * com.bumptech.glide.load.resource.gif.GifDrawable}) and ignores unknown types. * *

    This will override previous calls to {@link #dontTransform()}. * * @param transformation Any {@link Transformation} for {@link Bitmap}s. * @see #transform(Transformation) * @see #transform(Class, Transformation) */ // Guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings("CheckResult") @NonNull @CheckResult public T optionalTransform(@NonNull Transformation transformation) { return transform(transformation, /* isRequired= */ false); } @NonNull T transform(@NonNull Transformation transformation, boolean isRequired) { if (isAutoCloneEnabled) { return clone().transform(transformation, isRequired); } DrawableTransformation drawableTransformation = new DrawableTransformation(transformation, isRequired); transform(Bitmap.class, transformation, isRequired); transform(Drawable.class, drawableTransformation, isRequired); // TODO: remove BitmapDrawable decoder and this transformation. // Registering as BitmapDrawable is simply an optimization to avoid some iteration and // isAssignableFrom checks when obtaining the transformation later on. It can be removed without // affecting the functionality. transform(BitmapDrawable.class, drawableTransformation.asBitmapDrawable(), isRequired); transform(GifDrawable.class, new GifDrawableTransformation(transformation), isRequired); return selfOrThrowIfLocked(); } /** * Applies the given {@link Transformation} for any decoded resource of the given type and allows * unknown resource types to be ignored. * *

    Users can apply different transformations for each resource class. Applying a {@link * Transformation} for a resource type that already has a {@link Transformation} will override the * previous call. * *

    If any calls are made to the non-optional transform methods, then attempting to transform an * unknown resource class will throw an exception. To allow unknown types, users must always call * the optional version of each method. * *

    This will override previous calls to {@link #dontTransform()}. * * @param resourceClass The type of resource to transform. * @param transformation The {@link Transformation} to apply. */ @NonNull @CheckResult public T optionalTransform( @NonNull Class resourceClass, @NonNull Transformation transformation) { return transform(resourceClass, transformation, /* isRequired= */ false); } @NonNull T transform( @NonNull Class resourceClass, @NonNull Transformation transformation, boolean isRequired) { if (isAutoCloneEnabled) { return clone().transform(resourceClass, transformation, isRequired); } Preconditions.checkNotNull(resourceClass); Preconditions.checkNotNull(transformation); transformations.put(resourceClass, transformation); fields |= TRANSFORMATION; isTransformationAllowed = true; fields |= TRANSFORMATION_ALLOWED; // Always set to false here. Known scale only transformations will call this method and then // set isScaleOnlyOrNoTransform to true immediately after. isScaleOnlyOrNoTransform = false; if (isRequired) { fields |= TRANSFORMATION_REQUIRED; isTransformationRequired = true; } return selfOrThrowIfLocked(); } /** * Applies the given {@link Transformation} for any decoded resource of the given type and throws * if asked to transform an unknown resource type. * *

    This will override previous calls to {@link #dontTransform()}. * * @param resourceClass The type of resource to transform. * @param transformation The {@link Transformation} to apply. * @see #optionalTransform(Class, Transformation) */ // Guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings("CheckResult") @NonNull @CheckResult public T transform( @NonNull Class resourceClass, @NonNull Transformation transformation) { return transform(resourceClass, transformation, /* isRequired= */ true); } /** * Removes all applied {@link Transformation Transformations} for all resource classes and allows * unknown resource types to be transformed without throwing an exception. */ @NonNull @CheckResult public T dontTransform() { if (isAutoCloneEnabled) { return clone().dontTransform(); } transformations.clear(); fields &= ~TRANSFORMATION; isTransformationRequired = false; fields &= ~TRANSFORMATION_REQUIRED; isTransformationAllowed = false; fields |= TRANSFORMATION_ALLOWED; isScaleOnlyOrNoTransform = true; return selfOrThrowIfLocked(); } /** * Disables resource decoders that return animated resources so any resource returned will be * static. * *

    To disable transitions (fades etc) use {@link * com.bumptech.glide.TransitionOptions#dontTransition()} */ // Guaranteed to modify the current object by the isAutoCloneEnabledCheck. @SuppressWarnings("CheckResult") @NonNull @CheckResult public T dontAnimate() { return set(GifOptions.DISABLE_ANIMATION, true); } /** * Updates this options set with any options that are explicitly set in the given {@code T} object * and returns this object if {@link #autoClone()} is disabled or a new {@code T} object if {@link * #autoClone()} is enabled. * *

    {@code #apply} only replaces those values that are explicitly set in the given {@code T}. If * you need to completely reset all previously set options, create a new {@code T} object instead * of using this method. * *

    The options that will be set to values in the returned {@code T} object is the intersection * of the set of options in this {@code T} object and the given {@code T} object that were * explicitly set. If the values of any of the options conflict, the values in the returned {@code * T} object will be set to those in the given {@code T} object. */ @NonNull @CheckResult public T apply(@NonNull BaseRequestOptions o) { if (isAutoCloneEnabled) { return clone().apply(o); } BaseRequestOptions other = o; if (isSet(other.fields, SIZE_MULTIPLIER)) { sizeMultiplier = other.sizeMultiplier; } if (isSet(other.fields, USE_UNLIMITED_SOURCE_GENERATORS_POOL)) { useUnlimitedSourceGeneratorsPool = other.useUnlimitedSourceGeneratorsPool; } if (isSet(other.fields, USE_ANIMATION_POOL)) { useAnimationPool = other.useAnimationPool; } if (isSet(other.fields, DISK_CACHE_STRATEGY)) { diskCacheStrategy = other.diskCacheStrategy; } if (isSet(other.fields, PRIORITY)) { priority = other.priority; } if (isSet(other.fields, ERROR_PLACEHOLDER)) { errorPlaceholder = other.errorPlaceholder; errorId = 0; fields &= ~ERROR_ID; } if (isSet(other.fields, ERROR_ID)) { errorId = other.errorId; errorPlaceholder = null; fields &= ~ERROR_PLACEHOLDER; } if (isSet(other.fields, PLACEHOLDER)) { placeholderDrawable = other.placeholderDrawable; placeholderId = 0; fields &= ~PLACEHOLDER_ID; } if (isSet(other.fields, PLACEHOLDER_ID)) { placeholderId = other.placeholderId; placeholderDrawable = null; fields &= ~PLACEHOLDER; } if (isSet(other.fields, IS_CACHEABLE)) { isCacheable = other.isCacheable; } if (isSet(other.fields, OVERRIDE)) { overrideWidth = other.overrideWidth; overrideHeight = other.overrideHeight; } if (isSet(other.fields, SIGNATURE)) { signature = other.signature; } if (isSet(other.fields, RESOURCE_CLASS)) { resourceClass = other.resourceClass; } if (isSet(other.fields, FALLBACK)) { fallbackDrawable = other.fallbackDrawable; fallbackId = 0; fields &= ~FALLBACK_ID; } if (isSet(other.fields, FALLBACK_ID)) { fallbackId = other.fallbackId; fallbackDrawable = null; fields &= ~FALLBACK; } if (isSet(other.fields, THEME)) { theme = other.theme; } if (isSet(other.fields, TRANSFORMATION_ALLOWED)) { isTransformationAllowed = other.isTransformationAllowed; } if (isSet(other.fields, TRANSFORMATION_REQUIRED)) { isTransformationRequired = other.isTransformationRequired; } if (isSet(other.fields, TRANSFORMATION)) { transformations.putAll(other.transformations); isScaleOnlyOrNoTransform = other.isScaleOnlyOrNoTransform; } if (isSet(other.fields, ONLY_RETRIEVE_FROM_CACHE)) { onlyRetrieveFromCache = other.onlyRetrieveFromCache; } // Applying options with dontTransform() is expected to clear our transformations. if (!isTransformationAllowed) { transformations.clear(); fields &= ~TRANSFORMATION; isTransformationRequired = false; fields &= ~TRANSFORMATION_REQUIRED; isScaleOnlyOrNoTransform = true; } fields |= other.fields; options.putAll(other.options); return selfOrThrowIfLocked(); } /** * Returns {@code true} if this {@link BaseRequestOptions} is equivalent to the given {@link * BaseRequestOptions} (has all of the same options and sizes). * *

    This method is identical to {@link #equals(Object)}, but this can not be overridden. We need * to use this method instead of {@link #equals(Object)}, because child classes may have * additional fields, such as listeners and models, that should not be considered when checking * for equality. */ public final boolean isEquivalentTo(BaseRequestOptions other) { return Float.compare(other.sizeMultiplier, sizeMultiplier) == 0 && errorId == other.errorId && Util.bothNullOrEqual(errorPlaceholder, other.errorPlaceholder) && placeholderId == other.placeholderId && Util.bothNullOrEqual(placeholderDrawable, other.placeholderDrawable) && fallbackId == other.fallbackId && Util.bothNullOrEqual(fallbackDrawable, other.fallbackDrawable) && isCacheable == other.isCacheable && overrideHeight == other.overrideHeight && overrideWidth == other.overrideWidth && isTransformationRequired == other.isTransformationRequired && isTransformationAllowed == other.isTransformationAllowed && useUnlimitedSourceGeneratorsPool == other.useUnlimitedSourceGeneratorsPool && onlyRetrieveFromCache == other.onlyRetrieveFromCache && diskCacheStrategy.equals(other.diskCacheStrategy) && priority == other.priority && options.equals(other.options) && transformations.equals(other.transformations) && resourceClass.equals(other.resourceClass) && Util.bothNullOrEqual(signature, other.signature) && Util.bothNullOrEqual(theme, other.theme); } @Override public boolean equals(Object o) { if (o instanceof BaseRequestOptions) { return isEquivalentTo((BaseRequestOptions) o); } return false; } @Override public int hashCode() { int hashCode = Util.hashCode(sizeMultiplier); hashCode = Util.hashCode(errorId, hashCode); hashCode = Util.hashCode(errorPlaceholder, hashCode); hashCode = Util.hashCode(placeholderId, hashCode); hashCode = Util.hashCode(placeholderDrawable, hashCode); hashCode = Util.hashCode(fallbackId, hashCode); hashCode = Util.hashCode(fallbackDrawable, hashCode); hashCode = Util.hashCode(isCacheable, hashCode); hashCode = Util.hashCode(overrideHeight, hashCode); hashCode = Util.hashCode(overrideWidth, hashCode); hashCode = Util.hashCode(isTransformationRequired, hashCode); hashCode = Util.hashCode(isTransformationAllowed, hashCode); hashCode = Util.hashCode(useUnlimitedSourceGeneratorsPool, hashCode); hashCode = Util.hashCode(onlyRetrieveFromCache, hashCode); hashCode = Util.hashCode(diskCacheStrategy, hashCode); hashCode = Util.hashCode(priority, hashCode); hashCode = Util.hashCode(options, hashCode); hashCode = Util.hashCode(transformations, hashCode); hashCode = Util.hashCode(resourceClass, hashCode); hashCode = Util.hashCode(signature, hashCode); hashCode = Util.hashCode(theme, hashCode); return hashCode; } /** * Throws if any further mutations are attempted. * *

    Once locked, the only way to unlock is to use {@link #clone()} */ @NonNull @SuppressWarnings("unchecked") public T lock() { isLocked = true; // This is the only place we should not check locked. return self(); } /** * Similar to {@link #lock()} except that mutations cause a {@link #clone()} operation to happen * before the mutation resulting in all methods returning a new Object and leaving the original * locked object unmodified. * *

    Auto clone is not retained by cloned objects returned from mutations. The cloned objects are * mutable and are not locked. */ @NonNull public T autoClone() { if (isLocked && !isAutoCloneEnabled) { throw new IllegalStateException( "You cannot auto lock an already locked options object" + ", try clone() first"); } isAutoCloneEnabled = true; return lock(); } @NonNull @SuppressWarnings("unchecked") protected final T selfOrThrowIfLocked() { if (isLocked) { throw new IllegalStateException("You cannot modify locked T, consider clone()"); } return self(); } protected final boolean isAutoCloneEnabled() { return isAutoCloneEnabled; } public final boolean isDiskCacheStrategySet() { return isSet(DISK_CACHE_STRATEGY); } public final boolean isSkipMemoryCacheSet() { return isSet(IS_CACHEABLE); } @NonNull public final Map, Transformation> getTransformations() { return transformations; } @SuppressWarnings("WeakerAccess") public final boolean isTransformationRequired() { return isTransformationRequired; } @NonNull public final Options getOptions() { return options; } @NonNull public final Class getResourceClass() { return resourceClass; } @NonNull public final DiskCacheStrategy getDiskCacheStrategy() { return diskCacheStrategy; } @SuppressWarnings("WeakerAccess") @Nullable public final Drawable getErrorPlaceholder() { return errorPlaceholder; } @SuppressWarnings("WeakerAccess") public final int getErrorId() { return errorId; } @SuppressWarnings("WeakerAccess") public final int getPlaceholderId() { return placeholderId; } @SuppressWarnings("WeakerAccess") @Nullable public final Drawable getPlaceholderDrawable() { return placeholderDrawable; } @SuppressWarnings("WeakerAccess") public final int getFallbackId() { return fallbackId; } @SuppressWarnings("WeakerAccess") @Nullable public final Drawable getFallbackDrawable() { return fallbackDrawable; } @Nullable public final Resources.Theme getTheme() { return theme; } @SuppressWarnings("WeakerAccess") public final boolean isMemoryCacheable() { return isCacheable; } @NonNull public final Key getSignature() { return signature; } public final boolean isPrioritySet() { return isSet(PRIORITY); } @NonNull public final Priority getPriority() { return priority; } public final int getOverrideWidth() { return overrideWidth; } public final boolean isValidOverride() { return Util.isValidDimensions(overrideWidth, overrideHeight); } public final int getOverrideHeight() { return overrideHeight; } public final float getSizeMultiplier() { return sizeMultiplier; } boolean isScaleOnlyOrNoTransform() { return isScaleOnlyOrNoTransform; } private boolean isSet(int flag) { return isSet(fields, flag); } // get is just as clear. @SuppressWarnings("PMD.BooleanGetMethodName") public final boolean getUseUnlimitedSourceGeneratorsPool() { return useUnlimitedSourceGeneratorsPool; } // get is just as clear. @SuppressWarnings("PMD.BooleanGetMethodName") public final boolean getUseAnimationPool() { return useAnimationPool; } // get is just as clear. @SuppressWarnings("PMD.BooleanGetMethodName") public final boolean getOnlyRetrieveFromCache() { return onlyRetrieveFromCache; } @SuppressWarnings("unchecked") private T self() { return (T) this; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/ErrorRequestCoordinator.java ================================================ package com.bumptech.glide.request; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; /** * Runs a single primary {@link Request} until it completes and then a fallback error request only * if the single primary request fails. */ public final class ErrorRequestCoordinator implements RequestCoordinator, Request { private final Object requestLock; @Nullable private final RequestCoordinator parent; private volatile Request primary; private volatile Request error; @GuardedBy("requestLock") private RequestState primaryState = RequestState.CLEARED; @GuardedBy("requestLock") private RequestState errorState = RequestState.CLEARED; public ErrorRequestCoordinator(Object requestLock, @Nullable RequestCoordinator parent) { this.requestLock = requestLock; this.parent = parent; } public void setRequests(Request primary, Request error) { this.primary = primary; this.error = error; } @Override public void begin() { synchronized (requestLock) { if (primaryState != RequestState.RUNNING) { primaryState = RequestState.RUNNING; primary.begin(); } } } @Override public void clear() { synchronized (requestLock) { primaryState = RequestState.CLEARED; primary.clear(); // Don't check primary's failed state here because it will have been reset by the clear call // immediately before this. if (errorState != RequestState.CLEARED) { errorState = RequestState.CLEARED; error.clear(); } } } @Override public void pause() { synchronized (requestLock) { if (primaryState == RequestState.RUNNING) { primaryState = RequestState.PAUSED; primary.pause(); } if (errorState == RequestState.RUNNING) { errorState = RequestState.PAUSED; error.pause(); } } } @Override public boolean isRunning() { synchronized (requestLock) { return primaryState == RequestState.RUNNING || errorState == RequestState.RUNNING; } } @Override public boolean isComplete() { synchronized (requestLock) { return primaryState == RequestState.SUCCESS || errorState == RequestState.SUCCESS; } } @Override public boolean isCleared() { synchronized (requestLock) { return primaryState == RequestState.CLEARED && errorState == RequestState.CLEARED; } } @Override public boolean isEquivalentTo(Request o) { if (o instanceof ErrorRequestCoordinator) { ErrorRequestCoordinator other = (ErrorRequestCoordinator) o; return primary.isEquivalentTo(other.primary) && error.isEquivalentTo(other.error); } return false; } @Override public boolean canSetImage(Request request) { synchronized (requestLock) { // Only one of primary or error runs at a time, so if we've reached this point and nothing // else is broken, we should have nothing else to enforce. return parentCanSetImage(); } } @GuardedBy("requestLock") private boolean parentCanSetImage() { return parent == null || parent.canSetImage(this); } @Override public boolean canNotifyStatusChanged(Request request) { synchronized (requestLock) { return parentCanNotifyStatusChanged() && isValidRequestForStatusChanged(request); } } @Override public boolean canNotifyCleared(Request request) { synchronized (requestLock) { return parentCanNotifyCleared() && request.equals(primary); } } @GuardedBy("requestLock") private boolean parentCanNotifyCleared() { return parent == null || parent.canNotifyCleared(this); } @GuardedBy("requestLock") private boolean parentCanNotifyStatusChanged() { return parent == null || parent.canNotifyStatusChanged(this); } @GuardedBy("requestLock") private boolean isValidRequestForStatusChanged(Request request) { if (primaryState != RequestState.FAILED) { return request.equals(primary); } else { return request.equals(error) // We don't want to call onLoadStarted once for the primary request and then again // if it fails and the error request starts. It's already running, so we might as well // avoid the duplicate notification by only notifying about the error state when it's // final. && (errorState == RequestState.SUCCESS || errorState == RequestState.FAILED); } } @Override public boolean isAnyResourceSet() { synchronized (requestLock) { return primary.isAnyResourceSet() || error.isAnyResourceSet(); } } @Override public void onRequestSuccess(Request request) { synchronized (requestLock) { if (request.equals(primary)) { primaryState = RequestState.SUCCESS; } else if (request.equals(error)) { errorState = RequestState.SUCCESS; } if (parent != null) { parent.onRequestSuccess(this); } } } @Override public void onRequestFailed(Request request) { synchronized (requestLock) { if (!request.equals(error)) { primaryState = RequestState.FAILED; if (errorState != RequestState.RUNNING) { errorState = RequestState.RUNNING; error.begin(); } return; } errorState = RequestState.FAILED; if (parent != null) { parent.onRequestFailed(this); } } } @Override public RequestCoordinator getRoot() { synchronized (requestLock) { return parent != null ? parent.getRoot() : this; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/ExperimentalRequestListener.java ================================================ package com.bumptech.glide.request; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.request.target.Target; /** * An extension of {@link RequestListener} with additional parameters. * *

    All equivalent methods are called at the relevant time by Glide. Implementations therefore * should only implement one version of each method. * * @param The type of resource that will be loaded for the request. * @deprecated Not ready for public consumption, avoid using this class. It may be removed at any * time. */ @Deprecated public abstract class ExperimentalRequestListener implements RequestListener { public void onRequestStarted(Object model) {} /** * Identical to {@link #onResourceReady(Object, Object, Target, DataSource, boolean)} except that * {@code isAlternateCacheKey} is provided. * * @param isAlternateCacheKey True if the data was obtained from the disk cache using an alternate * cache key provided by a {@link com.bumptech.glide.load.model.ModelLoader} via {@link * com.bumptech.glide.load.model.ModelLoader.LoadData#alternateKeys}. Valid only if {@code * dataSource} is {@link DataSource#DATA_DISK_CACHE} or {@link * DataSource#RESOURCE_DISK_CACHE}. */ public abstract boolean onResourceReady( ResourceT resource, Object model, Target target, DataSource dataSource, boolean isFirstResource, boolean isAlternateCacheKey); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/FutureTarget.java ================================================ package com.bumptech.glide.request; import com.bumptech.glide.request.target.Target; import java.util.concurrent.Future; /** * An interface for an object that is both a {@link com.bumptech.glide.request.target.Target} and a * {@link java.util.concurrent.Future}. For example: * *

    {@code
     * FutureTarget futureTarget = Glide.with(fragment)
     *                                       .load("http://goo.gl/1asf12")
     *                                       .asBitmap()
     *                                       .into(250, 250);
     * Bitmap myBitmap = futureTarget.get();
     * ... // do things with bitmap and then release when finished:
     * futureTarget.cancel(false);
     * }
    * *

    Note - {@link #get()} and {@link #get(long, java.util.concurrent.TimeUnit)} must be called off * of the main thread or they will block forever. * * @param The type of resource this FutureTarget will retrieve. */ public interface FutureTarget extends Future, Target {} ================================================ FILE: library/src/main/java/com/bumptech/glide/request/Request.java ================================================ package com.bumptech.glide.request; /** A request that loads a resource for an {@link com.bumptech.glide.request.target.Target}. */ public interface Request { /** Starts an asynchronous load. */ void begin(); /** * Prevents any bitmaps being loaded from previous requests, releases any resources held by this * request, displays the current placeholder if one was provided, and marks the request as having * been cancelled. */ void clear(); /** * Similar to {@link #clear} for in progress requests (or portions of a request), but does nothing * if the request is already complete. * *

    Unlike {@link #clear()}, this method allows implementations to act differently on subparts * of a request. For example if a Request has both a thumbnail and a primary request and the * thumbnail portion of the request is complete, this method allows only the primary portion of * the request to be paused without clearing the previously completed thumbnail portion. */ void pause(); /** Returns true if this request is running and has not completed or failed. */ boolean isRunning(); /** Returns true if the request has completed successfully. */ boolean isComplete(); /** Returns true if the request has been cleared. */ boolean isCleared(); /** * Returns true if a resource is set, even if the request is not yet complete or the primary * request has failed. */ boolean isAnyResourceSet(); /** * Returns {@code true} if this {@link Request} is equivalent to the given {@link Request} (has * all of the same options and sizes). * *

    This method is identical to {@link Object#equals(Object)} except that it's specific to * {@link Request} subclasses. We do not use {@link Object#equals(Object)} directly because we * track {@link Request}s in collections like {@link java.util.Set} and it's perfectly legitimate * to have two different {@link Request} objects for two different {@link * com.bumptech.glide.request.target.Target}s (for example). Using a similar but different method * let's us selectively compare {@link Request} objects to each other when it's useful in specific * scenarios. */ boolean isEquivalentTo(Request other); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/RequestCoordinator.java ================================================ package com.bumptech.glide.request; /** * An interface for coordinating multiple requests with the same {@link * com.bumptech.glide.request.target.Target}. * *

    To avoid deadlock, implemenations must not call into individual {@link Request}s to * determine their state (ie do not call {@link Request#isCleared()} or {@link Request#isRunning()} * etc). Instead use {@link RequestState} and the various methods available on this interface and * {@link Request} to track states manually. */ public interface RequestCoordinator { /** * Returns true if the {@link Request} can display a loaded bitmap. * * @param request The {@link Request} requesting permission to display a bitmap. */ boolean canSetImage(Request request); /** * Returns true if the {@link Request} can display a placeholder. * * @param request The {@link Request} requesting permission to display a placeholder. */ boolean canNotifyStatusChanged(Request request); /** * Returns {@code true} if the {@link Request} can clear the {@link * com.bumptech.glide.request.target.Target}. */ boolean canNotifyCleared(Request request); /** * Returns true if any coordinated {@link Request} has successfully completed. * * @see Request#isComplete() */ boolean isAnyResourceSet(); /** Must be called when a {@link Request} coordinated by this object completes successfully. */ void onRequestSuccess(Request request); /** Must be called when a {@link Request} coordinated by this object fails. */ void onRequestFailed(Request request); /** Returns the top most parent {@code RequestCoordinator}. */ RequestCoordinator getRoot(); /** A simple state enum to keep track of the states of individual subrequests. */ enum RequestState { RUNNING(false), PAUSED(false), CLEARED(false), SUCCESS(true), FAILED(true); private final boolean isComplete; RequestState(boolean isComplete) { this.isComplete = isComplete; } boolean isComplete() { return isComplete; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/RequestFutureTarget.java ================================================ package com.bumptech.glide.request; import android.graphics.drawable.Drawable; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Util; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * A {@link java.util.concurrent.Future} implementation for Glide that can be used to load resources * in a blocking manner on background threads. * *

    Note - Unlike most targets, RequestFutureTargets can be used once and only once. Attempting to * reuse a RequestFutureTarget will probably result in undesirable behavior or exceptions. Instead * of reusing objects of this class, the pattern should be: * *

    {@code
     * FutureTarget target = null;
     * RequestManager requestManager = Glide.with(context);
     * try {
     *   target = requestManager
     *      .downloadOnly()
     *      .load(model)
     *      .submit();
     *   File downloadedFile = target.get();
     *   // ... do something with the file (usually throws IOException)
     * } catch (ExecutionException | InterruptedException | IOException e) {
     *   // ... bug reporting or recovery
     * } finally {
     *   // make sure to cancel pending operations and free resources
     *   if (target != null) {
     *     target.cancel(true); // mayInterruptIfRunning
     *   }
     * }
     * }
    * * The {@link #cancel(boolean)} call will cancel pending operations and make sure that any resources * used are recycled. * * @param The type of the resource that will be loaded. */ public class RequestFutureTarget implements FutureTarget, RequestListener { private static final Waiter DEFAULT_WAITER = new Waiter(); private final int width; private final int height; // Exists for testing only. private final boolean assertBackgroundThread; private final Waiter waiter; @GuardedBy("this") @Nullable private R resource; @GuardedBy("this") @Nullable private Request request; @GuardedBy("this") private boolean isCancelled; @GuardedBy("this") private boolean resultReceived; @GuardedBy("this") private boolean loadFailed; @GuardedBy("this") @Nullable private GlideException exception; /** Constructor for a RequestFutureTarget. Should not be used directly. */ public RequestFutureTarget(int width, int height) { this(width, height, true, DEFAULT_WAITER); } RequestFutureTarget(int width, int height, boolean assertBackgroundThread, Waiter waiter) { this.width = width; this.height = height; this.assertBackgroundThread = assertBackgroundThread; this.waiter = waiter; } @Override public boolean cancel(boolean mayInterruptIfRunning) { Request toClear = null; synchronized (this) { if (isDone()) { return false; } isCancelled = true; waiter.notifyAll(this); if (mayInterruptIfRunning) { toClear = request; request = null; } } // Avoid deadlock by clearing outside of the lock (b/138335419) if (toClear != null) { toClear.clear(); } return true; } @Override public synchronized boolean isCancelled() { return isCancelled; } @Override public synchronized boolean isDone() { return isCancelled || resultReceived || loadFailed; } @Override public R get() throws InterruptedException, ExecutionException { try { return doGet(null); } catch (TimeoutException e) { throw new AssertionError(e); } } @Override public R get(long time, @NonNull TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException { return doGet(timeUnit.toMillis(time)); } /** A callback that should never be invoked directly. */ @Override public void getSize(@NonNull SizeReadyCallback cb) { cb.onSizeReady(width, height); } @Override public void removeCallback(@NonNull SizeReadyCallback cb) { // Do nothing because we do not retain references to SizeReadyCallbacks. } @Override public synchronized void setRequest(@Nullable Request request) { this.request = request; } @Override @Nullable public synchronized Request getRequest() { return request; } /** A callback that should never be invoked directly. */ @Override public void onLoadCleared(@Nullable Drawable placeholder) { // Do nothing. } /** A callback that should never be invoked directly. */ @Override public void onLoadStarted(@Nullable Drawable placeholder) { // Do nothing. } /** A callback that should never be invoked directly. */ @Override public synchronized void onLoadFailed(@Nullable Drawable errorDrawable) { // Ignored, synchronized for backwards compatibility. } /** A callback that should never be invoked directly. */ @Override public synchronized void onResourceReady( @NonNull R resource, @Nullable Transition transition) { // Ignored, synchronized for backwards compatibility. } private synchronized R doGet(Long timeoutMillis) throws ExecutionException, InterruptedException, TimeoutException { if (assertBackgroundThread && !isDone()) { Util.assertBackgroundThread(); } if (isCancelled) { throw new CancellationException(); } else if (loadFailed) { throw new ExecutionException(exception); } else if (resultReceived) { return resource; } if (timeoutMillis == null) { waiter.waitForTimeout(this, 0); } else if (timeoutMillis > 0) { long now = System.currentTimeMillis(); long deadline = now + timeoutMillis; while (!isDone() && now < deadline) { waiter.waitForTimeout(this, deadline - now); now = System.currentTimeMillis(); } } if (Thread.interrupted()) { throw new InterruptedException(); } else if (loadFailed) { throw new ExecutionException(exception); } else if (isCancelled) { throw new CancellationException(); } else if (!resultReceived) { throw new TimeoutException(); } return resource; } @Override public void onStart() { // Do nothing. } @Override public void onStop() { // Do nothing. } @Override public void onDestroy() { // Do nothing. } @Override public synchronized boolean onLoadFailed( @Nullable GlideException e, Object model, Target target, boolean isFirstResource) { loadFailed = true; exception = e; waiter.notifyAll(this); return false; } @Override public synchronized boolean onResourceReady( R resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { // We might get a null result. resultReceived = true; this.resource = resource; waiter.notifyAll(this); return false; } @Override public String toString() { String toString = super.toString() + "[status="; final String status; Request pendingRequest = null; synchronized (this) { if (isCancelled) { status = "CANCELLED"; } else if (loadFailed) { status = "FAILURE"; } else if (resultReceived) { status = "SUCCESS"; } else { status = "PENDING"; pendingRequest = request; } } if (pendingRequest != null) { return toString + status + ", request=[" + pendingRequest + "]]"; } return toString + status + "]"; } @VisibleForTesting static class Waiter { // This is a simple wrapper class that is used to enable testing. The call to the wrapping class // is waited on appropriately. @SuppressWarnings("WaitNotInLoop") void waitForTimeout(Object toWaitOn, long timeoutMillis) throws InterruptedException { toWaitOn.wait(timeoutMillis); } void notifyAll(Object toNotify) { toNotify.notifyAll(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/RequestListener.java ================================================ package com.bumptech.glide.request; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.annotation.Nullable; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; /** * A class for monitoring the status of a request while images load. * *

    All methods in this interface will be called from a background thread if the {@code * RequestListener} is added to a request that is started with {@link RequestBuilder#submit()}, * {@link RequestBuilder#submit(int, int)}, or {@link RequestBuilder#into(int, int)}. Those methods * no longer post results back to the main thread to avoid the unnecessary thread interactions and * corresponding latency. As a side affect though, listeners added to those requests are no longer * called on the main thread. {@code RequestListeners} added to requests started with {@link * RequestBuilder#into(Target)} or {@link RequestBuilder#into(ImageView)} will continue to be called * back on the main thread. * * @param The type of resource being loaded. */ public interface RequestListener { /** * Called when an exception occurs during a load, immediately before {@link * Target#onLoadFailed(Drawable)}. Will only be called if we currently want to display an image * for the given model in the given target. It is recommended to create a single instance per * activity/fragment rather than instantiate a new object for each call to {@code * Glide.with(fragment/activity).load()} to avoid object churn. * *

    It is not safe to reload this or a different model in this callback. If you need to do so * use {@link com.bumptech.glide.RequestBuilder#error(RequestBuilder)} instead. * *

    Although you can't start an entirely new load, it is safe to change what is displayed in the * {@link Target} at this point, as long as you return {@code true} from the method to prevent * {@link Target#onLoadFailed(Drawable)} from being called. * *

    For threading guarantees, see the class comment. * *

    For example: * *

    {@code
       * public boolean onLoadFailed(Exception e, T model, Target target, boolean isFirstResource) {
       *     target.setPlaceholder(R.drawable.a_specific_error_for_my_exception);
       *     return true; // Prevent onLoadFailed from being called on the Target.
       * }
       * }
    * * @param e The maybe {@code null} exception containing information about why the request failed. * @param model The model we were trying to load when the exception occurred. * @param target The {@link Target} we were trying to load the image into. * @param isFirstResource {@code true} if this exception is for the first resource to load. * @return {@code true} to prevent {@link Target#onLoadFailed(Drawable)} from being called on * {@code target}, typically because the listener wants to update the {@code target} or the * object the {@code target} wraps itself or {@code false} to allow {@link * Target#onLoadFailed(Drawable)} to be called on {@code target}. */ boolean onLoadFailed( @Nullable GlideException e, Object model, Target target, boolean isFirstResource); /** * Called when a load completes successfully, immediately before {@link * Target#onResourceReady(Object, com.bumptech.glide.request.transition.Transition)}. * *

    For threading guarantees, see the class comment. * * @param resource The resource that was loaded for the target. Non-null because a null resource * will result in a call to {@link #onLoadFailed(GlideException, Object, Target, boolean)} * instead of this method. * @param model The specific model that was used to load the image. Non-null because a null model * will result in a call to {@link #onLoadFailed(GlideException, Object, Target, boolean)} * instead of this method. * @param target The target the model was loaded into. * @param dataSource The {@link DataSource} the resource was loaded from. * @param isFirstResource {@code true} if this is the first resource to in this load to be loaded * into the target. For example when loading a thumbnail and a full-sized image, this will be * {@code true} for the first image to load and {@code false} for the second. * @return {@code true} to prevent {@link Target#onResourceReady(Object, Transition)} from being * called on {@code target}, typically because the listener wants to update the {@code target} * or the object the {@code target} wraps itself or {@code false} to allow {@link * Target#onResourceReady(Object, Transition)} to be called on {@code target}. */ boolean onResourceReady( R resource, Object model, Target target, DataSource dataSource, boolean isFirstResource); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/RequestOptions.java ================================================ package com.bumptech.glide.request; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.CheckResult; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; /** * Provides type independent options to customize loads with Glide. * *

    Non-final to allow Glide's generated classes to be assignable to their non-generated * equivalents. */ @SuppressWarnings("PMD.UseUtilityClass") public class RequestOptions extends BaseRequestOptions { @Nullable private static RequestOptions skipMemoryCacheTrueOptions; @Nullable private static RequestOptions skipMemoryCacheFalseOptions; @Nullable private static RequestOptions fitCenterOptions; @Nullable private static RequestOptions centerInsideOptions; @Nullable private static RequestOptions centerCropOptions; @Nullable private static RequestOptions circleCropOptions; @Nullable private static RequestOptions noTransformOptions; @Nullable private static RequestOptions noAnimationOptions; /** Returns a {@link RequestOptions} object with {@link #sizeMultiplier(float)} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions sizeMultiplierOf( @FloatRange(from = 0, to = 1) float sizeMultiplier) { return new RequestOptions().sizeMultiplier(sizeMultiplier); } /** * Returns a {@link RequestOptions} object with {@link #diskCacheStrategy(DiskCacheStrategy)} set. */ @NonNull @CheckResult public static RequestOptions diskCacheStrategyOf(@NonNull DiskCacheStrategy diskCacheStrategy) { return new RequestOptions().diskCacheStrategy(diskCacheStrategy); } /** * Returns a {@link RequestOptions} object with {@link BaseRequestOptions#priority(Priority)}} * set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions priorityOf(@NonNull Priority priority) { return new RequestOptions().priority(priority); } /** Returns a {@link RequestOptions} object with {@link #placeholder(Drawable)} set. */ @NonNull @CheckResult public static RequestOptions placeholderOf(@Nullable Drawable placeholder) { return new RequestOptions().placeholder(placeholder); } /** Returns a {@link RequestOptions} object with {@link #placeholder(int)} set. */ @NonNull @CheckResult public static RequestOptions placeholderOf(@DrawableRes int placeholderId) { return new RequestOptions().placeholder(placeholderId); } /** Returns a {@link RequestOptions} object with {@link #error(Drawable)} set. */ @NonNull @CheckResult public static RequestOptions errorOf(@Nullable Drawable errorDrawable) { return new RequestOptions().error(errorDrawable); } /** Returns a {@link RequestOptions} object with {@link #error(int)}} set. */ @NonNull @CheckResult public static RequestOptions errorOf(@DrawableRes int errorId) { return new RequestOptions().error(errorId); } /** Returns a {@link RequestOptions} object with {@link #skipMemoryCache(boolean)} set. */ @NonNull @CheckResult public static RequestOptions skipMemoryCacheOf(boolean skipMemoryCache) { if (skipMemoryCache) { if (skipMemoryCacheTrueOptions == null) { skipMemoryCacheTrueOptions = new RequestOptions().skipMemoryCache(true).autoClone(); } return skipMemoryCacheTrueOptions; } else { if (skipMemoryCacheFalseOptions == null) { skipMemoryCacheFalseOptions = new RequestOptions().skipMemoryCache(false).autoClone(); } return skipMemoryCacheFalseOptions; } } /** Returns a {@link RequestOptions} object with {@link #override(int, int)}} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions overrideOf(int width, int height) { return new RequestOptions().override(width, height); } /** * Returns a {@link RequestOptions} with {@link #override(int, int)} set where both the width and * height are the given size. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions overrideOf(int size) { return overrideOf(size, size); } /** Returns a {@link RequestOptions} object with {@link #signature} set. */ @NonNull @CheckResult public static RequestOptions signatureOf(@NonNull Key signature) { return new RequestOptions().signature(signature); } /** Returns a {@link RequestOptions} object with {@link #fitCenter()} set. */ @NonNull @CheckResult public static RequestOptions fitCenterTransform() { if (fitCenterOptions == null) { fitCenterOptions = new RequestOptions().fitCenter().autoClone(); } return fitCenterOptions; } /** Returns a {@link RequestOptions} object with {@link #centerInside()} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions centerInsideTransform() { if (centerInsideOptions == null) { centerInsideOptions = new RequestOptions().centerInside().autoClone(); } return centerInsideOptions; } /** Returns a {@link RequestOptions} object with {@link #centerCrop()} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions centerCropTransform() { if (centerCropOptions == null) { centerCropOptions = new RequestOptions().centerCrop().autoClone(); } return centerCropOptions; } /** Returns a {@link RequestOptions} object with {@link RequestOptions#circleCrop()} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions circleCropTransform() { if (circleCropOptions == null) { circleCropOptions = new RequestOptions().circleCrop().autoClone(); } return circleCropOptions; } /** Returns a {@link RequestOptions} object with {@link #transform(Transformation)} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions bitmapTransform(@NonNull Transformation transformation) { return new RequestOptions().transform(transformation); } /** Returns a {@link RequestOptions} object with {@link #dontTransform()} set. */ @SuppressWarnings("WeakerAccess") @NonNull @CheckResult public static RequestOptions noTransformation() { if (noTransformOptions == null) { noTransformOptions = new RequestOptions().dontTransform().autoClone(); } return noTransformOptions; } /** * Returns a {@link RequestOptions} object with the given {@link Option} set via {@link * #set(Option, Object)}. */ @NonNull @CheckResult public static RequestOptions option(@NonNull Option option, @NonNull T value) { return new RequestOptions().set(option, value); } /** Returns a {@link RequestOptions} object with {@link #decode(Class)} set. */ @NonNull @CheckResult public static RequestOptions decodeTypeOf(@NonNull Class resourceClass) { return new RequestOptions().decode(resourceClass); } /** Returns a {@link RequestOptions} object with {@link #format(DecodeFormat)} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions formatOf(@NonNull DecodeFormat format) { return new RequestOptions().format(format); } /** Returns a {@link RequestOptions} object with {@link #frame(long)} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions frameOf(@IntRange(from = 0) long frameTimeMicros) { return new RequestOptions().frame(frameTimeMicros); } /** Returns a {@link RequestOptions} object with {@link #downsample(DownsampleStrategy)} set. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions downsampleOf(@NonNull DownsampleStrategy strategy) { return new RequestOptions().downsample(strategy); } /** Returns a {@link RequestOptions} object with {@link #timeout(int)} set. */ @NonNull @CheckResult public static RequestOptions timeoutOf(@IntRange(from = 0) int timeout) { return new RequestOptions().timeout(timeout); } /** * Returns a {@link com.bumptech.glide.request.RequestOptions} with {@link #encodeQuality(int)} * called with the given quality. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions encodeQualityOf(@IntRange(from = 0, to = 100) int quality) { return new RequestOptions().encodeQuality(quality); } /** * Returns a {@link com.bumptech.glide.request.RequestOptions} with {@link * #encodeFormat(android.graphics.Bitmap.CompressFormat)} called with the given format. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions encodeFormatOf(@NonNull Bitmap.CompressFormat format) { return new RequestOptions().encodeFormat(format); } /** * Returns a new {@link com.bumptech.glide.request.RequestOptions} with {@link #dontAnimate()} * called. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull @CheckResult public static RequestOptions noAnimation() { if (noAnimationOptions == null) { noAnimationOptions = new RequestOptions().dontAnimate().autoClone(); } return noAnimationOptions; } // Make sure that we're not equal to any other concrete implementation of RequestOptions. @Override public boolean equals(Object o) { return o instanceof RequestOptions && super.equals(o); } // Our class doesn't include any additional properties, so we don't need to modify hashcode, but // keep it here as a reminder in case we add properties. @SuppressWarnings("PMD.UselessOverridingMethod") @Override public int hashCode() { return super.hashCode(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/ResourceCallback.java ================================================ package com.bumptech.glide.request; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.Resource; /** * A callback that listens for when a resource load completes successfully or fails due to an * exception. */ public interface ResourceCallback { /** * Called when a resource is successfully loaded. * * @param resource The loaded resource. */ void onResourceReady( Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey); /** * Called when a resource fails to load successfully. * * @param e a non-null {@link GlideException}. */ void onLoadFailed(GlideException e); /** Returns the lock to use when notifying individual requests. */ Object getLock(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/SingleRequest.java ================================================ package com.bumptech.glide.request; import android.content.Context; import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.util.Log; import androidx.annotation.DrawableRes; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.GlideBuilder.LogRequestOrigins; import com.bumptech.glide.GlideContext; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.resource.drawable.DrawableDecoderCompat; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.request.transition.TransitionFactory; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Util; import com.bumptech.glide.util.pool.GlideTrace; import com.bumptech.glide.util.pool.StateVerifier; import java.util.List; import java.util.concurrent.Executor; /** * A {@link Request} that loads a {@link com.bumptech.glide.load.engine.Resource} into a given * {@link Target}. * * @param The type of the resource that will be transcoded from the loaded resource. */ public final class SingleRequest implements Request, SizeReadyCallback, ResourceCallback { /** Tag for logging internal events, not generally suitable for public use. */ private static final String TAG = "GlideRequest"; /** Tag for logging externally useful events (request completion, timing etc). */ private static final String GLIDE_TAG = "Glide"; private static final boolean IS_VERBOSE_LOGGABLE = Log.isLoggable(TAG, Log.VERBOSE); private int cookie; private enum Status { /** Created but not yet running. */ PENDING, /** In the process of fetching media. */ RUNNING, /** Waiting for a callback given to the Target to be called to determine target dimensions. */ WAITING_FOR_SIZE, /** Finished loading media successfully. */ COMPLETE, /** Failed to load media, may be restarted. */ FAILED, /** Cleared by the user with a placeholder set, may be restarted. */ CLEARED, } @Nullable private final String tag = IS_VERBOSE_LOGGABLE ? String.valueOf(super.hashCode()) : null; private final StateVerifier stateVerifier = StateVerifier.newInstance(); /* Variables mutated only when a request is initialized or returned to the object pool. */ private final Object requestLock; @Nullable private final RequestListener targetListener; private final RequestCoordinator requestCoordinator; private final Context context; private final GlideContext glideContext; @Nullable private final Object model; private final Class transcodeClass; private final BaseRequestOptions requestOptions; private final int overrideWidth; private final int overrideHeight; private final Priority priority; private final Target target; @Nullable private final List> requestListeners; private final TransitionFactory animationFactory; private final Executor callbackExecutor; @GuardedBy("requestLock") private Resource resource; @GuardedBy("requestLock") private Engine.LoadStatus loadStatus; @GuardedBy("requestLock") private long startTime; // Volatile because it's accessed outside of a lock and nullable, even though in practice it will // always be non-null unless the request is in the object pool. private volatile Engine engine; /* Variables mutated during a request. */ @GuardedBy("requestLock") private Status status; @GuardedBy("requestLock") @Nullable private Drawable errorDrawable; @GuardedBy("requestLock") @Nullable private Drawable placeholderDrawable; @GuardedBy("requestLock") @Nullable private Drawable fallbackDrawable; @GuardedBy("requestLock") private int width; @GuardedBy("requestLock") private int height; @GuardedBy("requestLock") private boolean isCallingCallbacks; @Nullable private RuntimeException requestOrigin; public static SingleRequest obtain( Context context, GlideContext glideContext, Object requestLock, Object model, Class transcodeClass, BaseRequestOptions requestOptions, int overrideWidth, int overrideHeight, Priority priority, Target target, RequestListener targetListener, @Nullable List> requestListeners, RequestCoordinator requestCoordinator, Engine engine, TransitionFactory animationFactory, Executor callbackExecutor) { return new SingleRequest<>( context, glideContext, requestLock, model, transcodeClass, requestOptions, overrideWidth, overrideHeight, priority, target, targetListener, requestListeners, requestCoordinator, engine, animationFactory, callbackExecutor); } // We are in fact locking on the same lock that will be used for all subsequent method calls. @SuppressWarnings("GuardedBy") private SingleRequest( Context context, GlideContext glideContext, @NonNull Object requestLock, @Nullable Object model, Class transcodeClass, BaseRequestOptions requestOptions, int overrideWidth, int overrideHeight, Priority priority, Target target, @Nullable RequestListener targetListener, @Nullable List> requestListeners, RequestCoordinator requestCoordinator, Engine engine, TransitionFactory animationFactory, Executor callbackExecutor) { this.requestLock = requestLock; this.context = context; this.glideContext = glideContext; this.model = model; this.transcodeClass = transcodeClass; this.requestOptions = requestOptions; this.overrideWidth = overrideWidth; this.overrideHeight = overrideHeight; this.priority = priority; this.target = target; this.targetListener = targetListener; this.requestListeners = requestListeners; this.requestCoordinator = requestCoordinator; this.engine = engine; this.animationFactory = animationFactory; this.callbackExecutor = callbackExecutor; status = Status.PENDING; if (requestOrigin == null && glideContext.getExperiments().isEnabled(LogRequestOrigins.class)) { requestOrigin = new RuntimeException("Glide request origin trace"); } } @Override public void begin() { synchronized (requestLock) { assertNotCallingCallbacks(); stateVerifier.throwIfRecycled(); startTime = LogTime.getLogTime(); if (model == null) { if (Util.isValidDimensions(overrideWidth, overrideHeight)) { width = overrideWidth; height = overrideHeight; } // Only log at more verbose log levels if the user has set a fallback drawable, because // fallback Drawables indicate the user expects null models occasionally. int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG; onLoadFailed(new GlideException("Received null model"), logLevel); return; } if (status == Status.RUNNING) { throw new IllegalArgumentException("Cannot restart a running request"); } // If we're restarted after we're complete (usually via something like a notifyDataSetChanged // that starts an identical request into the same Target or View), we can simply use the // resource and size we retrieved the last time around and skip obtaining a new size, starting // a new load etc. This does mean that users who want to restart a load because they expect // that the view size has changed will need to explicitly clear the View or Target before // starting the new load. if (status == Status.COMPLETE) { onResourceReady( resource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false); return; } // Restarts for requests that are neither complete nor running can be treated as new requests // and can run again from the beginning. experimentalNotifyRequestStarted(model); cookie = GlideTrace.beginSectionAsync(TAG); status = Status.WAITING_FOR_SIZE; if (Util.isValidDimensions(overrideWidth, overrideHeight)) { onSizeReady(overrideWidth, overrideHeight); } else { target.getSize(this); } if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE) && canNotifyStatusChanged()) { target.onLoadStarted(getPlaceholderDrawable()); } if (IS_VERBOSE_LOGGABLE) { logV("finished run method in " + LogTime.getElapsedMillis(startTime)); } } } private void experimentalNotifyRequestStarted(Object model) { if (requestListeners == null) { return; } for (RequestListener requestListener : requestListeners) { if (requestListener instanceof ExperimentalRequestListener) { ((ExperimentalRequestListener) requestListener).onRequestStarted(model); } } } /** * Cancels the current load but does not release any resources held by the request and continues * to display the loaded resource if the load completed before the call to cancel. * *

    Cancelled requests can be restarted with a subsequent call to {@link #begin()}. * * @see #clear() */ @GuardedBy("requestLock") private void cancel() { assertNotCallingCallbacks(); stateVerifier.throwIfRecycled(); target.removeCallback(this); if (loadStatus != null) { loadStatus.cancel(); loadStatus = null; } } // Avoids difficult to understand errors like #2413. @GuardedBy("requestLock") private void assertNotCallingCallbacks() { if (isCallingCallbacks) { throw new IllegalStateException( "You can't start or clear loads in RequestListener or" + " Target callbacks. If you're trying to start a fallback request when a load fails," + " use RequestBuilder#error(RequestBuilder). Otherwise consider posting your into()" + " or clear() calls to the main thread using a Handler instead."); } } /** * Cancels the current load if it is in progress, clears any resources held onto by the request * and replaces the loaded resource if the load completed with the placeholder. * *

    Cleared requests can be restarted with a subsequent call to {@link #begin()} * * @see #cancel() */ @Override public void clear() { Resource toRelease = null; synchronized (requestLock) { assertNotCallingCallbacks(); stateVerifier.throwIfRecycled(); if (status == Status.CLEARED) { return; } cancel(); // Resource must be released before canNotifyStatusChanged is called. if (resource != null) { toRelease = resource; resource = null; } if (canNotifyCleared()) { target.onLoadCleared(getPlaceholderDrawable()); } GlideTrace.endSectionAsync(TAG, cookie); status = Status.CLEARED; } if (toRelease != null) { engine.release(toRelease); } } @Override public void pause() { synchronized (requestLock) { if (isRunning()) { clear(); } } } @Override public boolean isRunning() { synchronized (requestLock) { return status == Status.RUNNING || status == Status.WAITING_FOR_SIZE; } } @Override public boolean isComplete() { synchronized (requestLock) { return status == Status.COMPLETE; } } @Override public boolean isCleared() { synchronized (requestLock) { return status == Status.CLEARED; } } @Override public boolean isAnyResourceSet() { synchronized (requestLock) { return status == Status.COMPLETE; } } @GuardedBy("requestLock") private Drawable getErrorDrawable() { if (errorDrawable == null) { errorDrawable = requestOptions.getErrorPlaceholder(); if (errorDrawable == null && requestOptions.getErrorId() > 0) { errorDrawable = loadDrawable(requestOptions.getErrorId()); } } return errorDrawable; } @GuardedBy("requestLock") private Drawable getPlaceholderDrawable() { if (placeholderDrawable == null) { placeholderDrawable = requestOptions.getPlaceholderDrawable(); if (placeholderDrawable == null && requestOptions.getPlaceholderId() > 0) { placeholderDrawable = loadDrawable(requestOptions.getPlaceholderId()); } } return placeholderDrawable; } @GuardedBy("requestLock") private Drawable getFallbackDrawable() { if (fallbackDrawable == null) { fallbackDrawable = requestOptions.getFallbackDrawable(); if (fallbackDrawable == null && requestOptions.getFallbackId() > 0) { fallbackDrawable = loadDrawable(requestOptions.getFallbackId()); } } return fallbackDrawable; } @GuardedBy("requestLock") private Drawable loadDrawable(@DrawableRes int resourceId) { Theme theme = requestOptions.getTheme() != null ? requestOptions.getTheme() : context.getTheme(); return DrawableDecoderCompat.getDrawable(context, resourceId, theme); } @GuardedBy("requestLock") private void setErrorPlaceholder() { if (!canNotifyStatusChanged()) { return; } Drawable error = null; if (model == null) { error = getFallbackDrawable(); } // Either the model isn't null, or there was no fallback drawable set. if (error == null) { error = getErrorDrawable(); } // The model isn't null, no fallback drawable was set or no error drawable was set. if (error == null) { error = getPlaceholderDrawable(); } target.onLoadFailed(error); } /** A callback method that should never be invoked directly. */ @Override public void onSizeReady(int width, int height) { stateVerifier.throwIfRecycled(); synchronized (requestLock) { if (IS_VERBOSE_LOGGABLE) { logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime)); } if (status != Status.WAITING_FOR_SIZE) { return; } status = Status.RUNNING; float sizeMultiplier = requestOptions.getSizeMultiplier(); this.width = maybeApplySizeMultiplier(width, sizeMultiplier); this.height = maybeApplySizeMultiplier(height, sizeMultiplier); if (IS_VERBOSE_LOGGABLE) { logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime)); } loadStatus = engine.load( glideContext, model, requestOptions.getSignature(), this.width, this.height, requestOptions.getResourceClass(), transcodeClass, priority, requestOptions.getDiskCacheStrategy(), requestOptions.getTransformations(), requestOptions.isTransformationRequired(), requestOptions.isScaleOnlyOrNoTransform(), requestOptions.getOptions(), requestOptions.isMemoryCacheable(), requestOptions.getUseUnlimitedSourceGeneratorsPool(), requestOptions.getUseAnimationPool(), requestOptions.getOnlyRetrieveFromCache(), this, callbackExecutor); // This is a hack that's only useful for testing right now where loads complete synchronously // even though under any executor running on any thread but the main thread, the load would // have completed asynchronously. if (status != Status.RUNNING) { loadStatus = null; } if (IS_VERBOSE_LOGGABLE) { logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime)); } } } private static int maybeApplySizeMultiplier(int size, float sizeMultiplier) { return size == Target.SIZE_ORIGINAL ? size : Math.round(sizeMultiplier * size); } @GuardedBy("requestLock") private boolean canSetResource() { return requestCoordinator == null || requestCoordinator.canSetImage(this); } @GuardedBy("requestLock") private boolean canNotifyCleared() { return requestCoordinator == null || requestCoordinator.canNotifyCleared(this); } @GuardedBy("requestLock") private boolean canNotifyStatusChanged() { return requestCoordinator == null || requestCoordinator.canNotifyStatusChanged(this); } @GuardedBy("requestLock") private boolean isFirstReadyResource() { return requestCoordinator == null || !requestCoordinator.getRoot().isAnyResourceSet(); } @GuardedBy("requestLock") private void notifyRequestCoordinatorLoadSucceeded() { if (requestCoordinator != null) { requestCoordinator.onRequestSuccess(this); } } @GuardedBy("requestLock") private void notifyRequestCoordinatorLoadFailed() { if (requestCoordinator != null) { requestCoordinator.onRequestFailed(this); } } /** A callback method that should never be invoked directly. */ @SuppressWarnings("unchecked") @Override public void onResourceReady( Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) { stateVerifier.throwIfRecycled(); Resource toRelease = null; try { synchronized (requestLock) { loadStatus = null; if (resource == null) { GlideException exception = new GlideException( "Expected to receive a Resource with an " + "object of " + transcodeClass + " inside, but instead got null."); onLoadFailed(exception); return; } Object received = resource.get(); if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) { toRelease = resource; this.resource = null; GlideException exception = new GlideException( "Expected to receive an object of " + transcodeClass + " but instead" + " got " + (received != null ? received.getClass() : "") + "{" + received + "} inside" + " " + "Resource{" + resource + "}." + (received != null ? "" : " " + "To indicate failure return a null Resource " + "object, rather than a Resource object containing null data.")); onLoadFailed(exception); return; } if (!canSetResource()) { toRelease = resource; this.resource = null; // We can't put the status to complete before asking canSetResource(). status = Status.COMPLETE; GlideTrace.endSectionAsync(TAG, cookie); return; } onResourceReady( (Resource) resource, (R) received, dataSource, isLoadedFromAlternateCacheKey); } } finally { if (toRelease != null) { engine.release(toRelease); } } } /** * Internal {@link #onResourceReady(Resource, DataSource, boolean)} where arguments are known to * be safe. * * @param resource original {@link Resource}, never null * @param result object returned by {@link Resource#get()}, checked for type and never null * */ // We're using experimental APIs... @SuppressWarnings({"deprecation", "PMD.UnusedFormalParameter"}) @GuardedBy("requestLock") private void onResourceReady( Resource resource, R result, DataSource dataSource, boolean isAlternateCacheKey) { // We must call isFirstReadyResource before setting status. boolean isFirstResource = isFirstReadyResource(); status = Status.COMPLETE; this.resource = resource; if (glideContext.getLogLevel() <= Log.DEBUG) { Log.d( GLIDE_TAG, "Finished loading " + result.getClass().getSimpleName() + " from " + dataSource + " for " + model + " with size [" + width + "x" + height + "] in " + LogTime.getElapsedMillis(startTime) + " ms"); } notifyRequestCoordinatorLoadSucceeded(); isCallingCallbacks = true; try { boolean anyListenerHandledUpdatingTarget = false; if (requestListeners != null) { for (RequestListener listener : requestListeners) { anyListenerHandledUpdatingTarget |= listener.onResourceReady(result, model, target, dataSource, isFirstResource); if (listener instanceof ExperimentalRequestListener) { ExperimentalRequestListener experimentalRequestListener = (ExperimentalRequestListener) listener; anyListenerHandledUpdatingTarget |= experimentalRequestListener.onResourceReady( result, model, target, dataSource, isFirstResource, isAlternateCacheKey); } } } anyListenerHandledUpdatingTarget |= targetListener != null && targetListener.onResourceReady(result, model, target, dataSource, isFirstResource); if (!anyListenerHandledUpdatingTarget) { Transition animation = animationFactory.build(dataSource, isFirstResource); target.onResourceReady(result, animation); } } finally { isCallingCallbacks = false; } GlideTrace.endSectionAsync(TAG, cookie); } /** A callback method that should never be invoked directly. */ @Override public void onLoadFailed(GlideException e) { onLoadFailed(e, Log.WARN); } @Override public Object getLock() { stateVerifier.throwIfRecycled(); return requestLock; } private void onLoadFailed(GlideException e, int maxLogLevel) { stateVerifier.throwIfRecycled(); synchronized (requestLock) { e.setOrigin(requestOrigin); int logLevel = glideContext.getLogLevel(); if (logLevel <= maxLogLevel) { Log.w( GLIDE_TAG, "Load failed for [" + model + "] with dimensions [" + width + "x" + height + "]", e); if (logLevel <= Log.INFO) { e.logRootCauses(GLIDE_TAG); } } loadStatus = null; status = Status.FAILED; notifyRequestCoordinatorLoadFailed(); isCallingCallbacks = true; try { // TODO: what if this is a thumbnail request? boolean anyListenerHandledUpdatingTarget = false; if (requestListeners != null) { for (RequestListener listener : requestListeners) { anyListenerHandledUpdatingTarget |= listener.onLoadFailed(e, model, target, isFirstReadyResource()); } } anyListenerHandledUpdatingTarget |= targetListener != null && targetListener.onLoadFailed(e, model, target, isFirstReadyResource()); if (!anyListenerHandledUpdatingTarget) { setErrorPlaceholder(); } } finally { isCallingCallbacks = false; } GlideTrace.endSectionAsync(TAG, cookie); } } @Override public boolean isEquivalentTo(Request o) { if (!(o instanceof SingleRequest)) { return false; } int localOverrideWidth; int localOverrideHeight; Object localModel; Class localTranscodeClass; BaseRequestOptions localRequestOptions; Priority localPriority; int localListenerCount; synchronized (requestLock) { localOverrideWidth = overrideWidth; localOverrideHeight = overrideHeight; localModel = model; localTranscodeClass = transcodeClass; localRequestOptions = requestOptions; localPriority = priority; localListenerCount = requestListeners != null ? requestListeners.size() : 0; } SingleRequest other = (SingleRequest) o; int otherLocalOverrideWidth; int otherLocalOverrideHeight; Object otherLocalModel; Class otherLocalTranscodeClass; BaseRequestOptions otherLocalRequestOptions; Priority otherLocalPriority; int otherLocalListenerCount; synchronized (other.requestLock) { otherLocalOverrideWidth = other.overrideWidth; otherLocalOverrideHeight = other.overrideHeight; otherLocalModel = other.model; otherLocalTranscodeClass = other.transcodeClass; otherLocalRequestOptions = other.requestOptions; otherLocalPriority = other.priority; otherLocalListenerCount = other.requestListeners != null ? other.requestListeners.size() : 0; } // If there's ever a case where synchronization matters for these values, something else has // gone wrong. It indicates that we'er comparing at least one recycled object, which has to be // protected against via other means. None of these values changes aside from object re-use. return localOverrideWidth == otherLocalOverrideWidth && localOverrideHeight == otherLocalOverrideHeight && Util.bothModelsNullEquivalentOrEquals(localModel, otherLocalModel) && localTranscodeClass.equals(otherLocalTranscodeClass) && Util.bothBaseRequestOptionsNullEquivalentOrEquals( localRequestOptions, otherLocalRequestOptions) && localPriority == otherLocalPriority // We do not want to require that RequestListeners implement equals/hashcode, so we // don't compare them using equals(). We can however, at least assert that the same // amount of request listeners are present in both requests. && localListenerCount == otherLocalListenerCount; } private void logV(String message) { Log.v(TAG, message + " this: " + tag); } @Override public String toString() { Object localModel; Class localTranscodeClass; synchronized (requestLock) { localModel = model; localTranscodeClass = transcodeClass; } return super.toString() + "[model=" + localModel + ", transcodeClass=" + localTranscodeClass + "]"; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/ThumbnailRequestCoordinator.java ================================================ package com.bumptech.glide.request; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; /** * A coordinator that coordinates two individual {@link Request}s that load a small thumbnail * version of an image and the full size version of the image at the same time. */ public class ThumbnailRequestCoordinator implements RequestCoordinator, Request { @Nullable private final RequestCoordinator parent; private final Object requestLock; private volatile Request full; private volatile Request thumb; @GuardedBy("requestLock") private RequestState fullState = RequestState.CLEARED; @GuardedBy("requestLock") private RequestState thumbState = RequestState.CLEARED; // Only used to check if the full request is cleared by the thumbnail request. @GuardedBy("requestLock") private boolean isRunningDuringBegin; public ThumbnailRequestCoordinator(Object requestLock, @Nullable RequestCoordinator parent) { this.requestLock = requestLock; this.parent = parent; } public void setRequests(Request full, Request thumb) { this.full = full; this.thumb = thumb; } /** * Returns true if the request is either the request loading the full size image or if the request * loading the full size image has not yet completed. * * @param request {@inheritDoc} */ @Override public boolean canSetImage(Request request) { synchronized (requestLock) { return parentCanSetImage() && (request.equals(full) || fullState != RequestState.SUCCESS); } } @GuardedBy("requestLock") private boolean parentCanSetImage() { return parent == null || parent.canSetImage(this); } /** * Returns true if the request is the request loading the full size image and if neither the full * nor the thumbnail image have completed successfully. * * @param request {@inheritDoc}. */ @Override public boolean canNotifyStatusChanged(Request request) { synchronized (requestLock) { return parentCanNotifyStatusChanged() && request.equals(full) && !isAnyResourceSet(); } } @Override public boolean canNotifyCleared(Request request) { synchronized (requestLock) { return parentCanNotifyCleared() && request.equals(full) && fullState != RequestState.PAUSED; } } @GuardedBy("requestLock") private boolean parentCanNotifyCleared() { return parent == null || parent.canNotifyCleared(this); } @GuardedBy("requestLock") private boolean parentCanNotifyStatusChanged() { return parent == null || parent.canNotifyStatusChanged(this); } @Override public boolean isAnyResourceSet() { synchronized (requestLock) { return thumb.isAnyResourceSet() || full.isAnyResourceSet(); } } @Override public void onRequestSuccess(Request request) { synchronized (requestLock) { if (request.equals(thumb)) { thumbState = RequestState.SUCCESS; return; } fullState = RequestState.SUCCESS; if (parent != null) { parent.onRequestSuccess(this); } // Clearing the thumb is not necessarily safe if the thumb is being displayed in the Target, // as a layer in a cross fade for example. The only way we know the thumb is not being // displayed and is therefore safe to clear is if the thumb request has not yet completed. if (!thumbState.isComplete()) { thumb.clear(); } } } @Override public void onRequestFailed(Request request) { synchronized (requestLock) { if (!request.equals(full)) { thumbState = RequestState.FAILED; return; } fullState = RequestState.FAILED; if (parent != null) { parent.onRequestFailed(this); } } } @Override public RequestCoordinator getRoot() { synchronized (requestLock) { return parent != null ? parent.getRoot() : this; } } /** Starts first the thumb request and then the full request. */ @Override public void begin() { synchronized (requestLock) { isRunningDuringBegin = true; try { // If the request has completed previously, there's no need to restart both the full and the // thumb, we can just restart the full. if (fullState != RequestState.SUCCESS && thumbState != RequestState.RUNNING) { thumbState = RequestState.RUNNING; thumb.begin(); } if (isRunningDuringBegin && fullState != RequestState.RUNNING) { fullState = RequestState.RUNNING; full.begin(); } } finally { isRunningDuringBegin = false; } } } @Override public void clear() { synchronized (requestLock) { isRunningDuringBegin = false; fullState = RequestState.CLEARED; thumbState = RequestState.CLEARED; thumb.clear(); full.clear(); } } @Override public void pause() { synchronized (requestLock) { if (!thumbState.isComplete()) { thumbState = RequestState.PAUSED; thumb.pause(); } if (!fullState.isComplete()) { fullState = RequestState.PAUSED; full.pause(); } } } @Override public boolean isRunning() { synchronized (requestLock) { return fullState == RequestState.RUNNING; } } @Override public boolean isComplete() { synchronized (requestLock) { return fullState == RequestState.SUCCESS; } } @Override public boolean isCleared() { synchronized (requestLock) { return fullState == RequestState.CLEARED; } } @Override public boolean isEquivalentTo(Request o) { if (o instanceof ThumbnailRequestCoordinator) { ThumbnailRequestCoordinator that = (ThumbnailRequestCoordinator) o; return (full == null ? that.full == null : full.isEquivalentTo(that.full)) && (thumb == null ? that.thumb == null : thumb.isEquivalentTo(that.thumb)); } return false; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/AppWidgetTarget.java ================================================ package com.bumptech.glide.request.target; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Preconditions; /** * This class is used in order to display downloaded Bitmap inside an ImageView of an AppWidget * through RemoteViews. * *

    Note - For cancellation to work correctly, you must pass in the same instance of this class * for every subsequent load. */ // Public API. @SuppressWarnings("WeakerAccess") public class AppWidgetTarget extends CustomTarget { private final int[] widgetIds; private final ComponentName componentName; private final RemoteViews remoteViews; private final Context context; private final int viewId; /** * Constructor using an int array of widgetIds to get a handle on the Widget in order to update * it. * * @param context Context to use in the AppWidgetManager initialization. * @param width Desired width in pixels of the bitmap that will be loaded. (Needs to be manually * put because of RemoteViews limitations.) * @param height Desired height in pixels of the bitmap that will be loaded. (Needs to be manually * put because of RemoteViews limitations.) * @param viewId The id of the ImageView view that will load the image. * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. * @param widgetIds The int[] that contains the widget ids of an application. */ public AppWidgetTarget( Context context, int width, int height, int viewId, RemoteViews remoteViews, int... widgetIds) { super(width, height); if (widgetIds.length == 0) { throw new IllegalArgumentException("WidgetIds must have length > 0"); } this.context = Preconditions.checkNotNull(context, "Context can not be null!"); this.remoteViews = Preconditions.checkNotNull(remoteViews, "RemoteViews object can not be null!"); this.widgetIds = Preconditions.checkNotNull(widgetIds, "WidgetIds can not be null!"); this.viewId = viewId; componentName = null; } /** * Constructor using an int array of widgetIds to get a handle on the Widget in order to update it * that uses {@link #SIZE_ORIGINAL} as the target width and height. * * @param context Context to use in the AppWidgetManager initialization. * @param viewId The id of the ImageView view that will load the image. * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. * @param widgetIds The int[] that contains the widget ids of an application. */ public AppWidgetTarget(Context context, int viewId, RemoteViews remoteViews, int... widgetIds) { this(context, SIZE_ORIGINAL, SIZE_ORIGINAL, viewId, remoteViews, widgetIds); } /** * Constructor using a ComponentName to get a handle on the Widget in order to update it. * * @param context Context to use in the AppWidgetManager initialization. * @param width Desired width in pixels of the bitmap that will be loaded. (Needs to be manually * put because of RemoteViews limitations.) * @param height Desired height in pixels of the bitmap that will be loaded. (Needs to be manually * put because of RemoteViews limitations.) * @param viewId The id of the ImageView view that will load the image. * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. * @param componentName The ComponentName that refers to our AppWidget. */ public AppWidgetTarget( Context context, int width, int height, int viewId, RemoteViews remoteViews, ComponentName componentName) { super(width, height); this.context = Preconditions.checkNotNull(context, "Context can not be null!"); this.remoteViews = Preconditions.checkNotNull(remoteViews, "RemoteViews object can not be null!"); this.componentName = Preconditions.checkNotNull(componentName, "ComponentName can not be null!"); this.viewId = viewId; widgetIds = null; } /** * Constructor using a ComponentName, when override has been put to get a handle on the Widget in * order to update it that uses {@link #SIZE_ORIGINAL} as the target width and height. * * @param context Context to use in the AppWidgetManager initialization. * @param viewId The id of the ImageView view that will load the image. * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. * @param componentName The ComponentName that refers to our AppWidget. */ public AppWidgetTarget( Context context, int viewId, RemoteViews remoteViews, ComponentName componentName) { this(context, SIZE_ORIGINAL, SIZE_ORIGINAL, viewId, remoteViews, componentName); } /** Updates the AppWidget after the ImageView has loaded the Bitmap. */ private void update() { AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this.context); if (this.componentName != null) { appWidgetManager.updateAppWidget(this.componentName, this.remoteViews); } else { appWidgetManager.updateAppWidget(this.widgetIds, this.remoteViews); } } @Override public void onResourceReady( @NonNull Bitmap resource, @Nullable Transition transition) { setBitmap(resource); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { setBitmap(null); } private void setBitmap(@Nullable Bitmap bitmap) { this.remoteViews.setImageViewBitmap(viewId, bitmap); update(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/BaseTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Drawable; import androidx.annotation.Nullable; import com.bumptech.glide.request.Request; /** * A base {@link Target} for loading {@link com.bumptech.glide.load.engine.Resource}s that provides * basic or empty implementations for most methods. * *

    For maximum efficiency, clear this target when you have finished using or displaying the * {@link com.bumptech.glide.load.engine.Resource} loaded into it using {@link * com.bumptech.glide.RequestManager#clear(Target)}. * *

    For loading {@link com.bumptech.glide.load.engine.Resource}s into {@link android.view.View}s, * {@link com.bumptech.glide.request.target.ViewTarget} or {@link * com.bumptech.glide.request.target.ImageViewTarget} are preferable. * * @param The type of resource that will be received by this target. * @deprecated Use {@link CustomViewTarget} if loading the content into a view, the download API if * in the background * (http://bumptech.github.io/glide/doc/getting-started.html#background-threads), or a a fully * implemented {@link Target} for any specialized use-cases. Using BaseView is unsafe if the * user does not implement {@link #onLoadCleared}, resulting in recycled bitmaps being * referenced from the UI and hard to root-cause crashes. */ @Deprecated public abstract class BaseTarget implements Target { private Request request; @Override public void setRequest(@Nullable Request request) { this.request = request; } @Override @Nullable public Request getRequest() { return request; } @Override public void onLoadCleared(@Nullable Drawable placeholder) { // Do nothing. } @Override public void onLoadStarted(@Nullable Drawable placeholder) { // Do nothing. } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { // Do nothing. } @Override public void onStart() { // Do nothing. } @Override public void onStop() { // Do nothing. } @Override public void onDestroy() { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/BitmapImageViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.Bitmap; import android.widget.ImageView; /** * A {@link com.bumptech.glide.request.target.Target} that can display an {@link * android.graphics.Bitmap} in an {@link android.widget.ImageView}. */ public class BitmapImageViewTarget extends ImageViewTarget { // Public API. @SuppressWarnings("WeakerAccess") public BitmapImageViewTarget(ImageView view) { super(view); } /** * @deprecated Use {@link #waitForLayout()} instead. */ // Public API. @SuppressWarnings({"unused", "deprecation"}) @Deprecated public BitmapImageViewTarget(ImageView view, boolean waitForLayout) { super(view, waitForLayout); } /** * Sets the {@link android.graphics.Bitmap} on the view using {@link * android.widget.ImageView#setImageBitmap(android.graphics.Bitmap)}. * * @param resource The bitmap to display. */ @Override protected void setResource(Bitmap resource) { view.setImageBitmap(resource); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/BitmapThumbnailImageViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.widget.ImageView; /** * Efficiently displays multiple Bitmaps loaded serially into a single {@link android.view.View}. */ // Public API. @SuppressWarnings("unused") public class BitmapThumbnailImageViewTarget extends ThumbnailImageViewTarget { public BitmapThumbnailImageViewTarget(ImageView view) { super(view); } /** * @deprecated Use {@link #waitForLayout()} instead. */ @SuppressWarnings("deprecation") @Deprecated public BitmapThumbnailImageViewTarget(ImageView view, boolean waitForLayout) { super(view, waitForLayout); } @Override protected Drawable getDrawable(Bitmap resource) { return new BitmapDrawable(view.getResources(), resource); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/CustomTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Util; /** * A base {@link Target} for loading resources ({@link android.graphics.Bitmap}, {@link Drawable} * etc) that are used outside of {@link android.view.View}s. * *

    If you're loading a resource into a {@link View}, use {@link * com.bumptech.glide.RequestBuilder#into(ImageView)}, a subclass of {@link ImageViewTarget}, or * {@link CustomViewTarget}. Using this class to load resources into {@link View}s can prevent Glide * from correctly cancelling any previous loads, which may result in incorrect images appearing in * the view, especially in scrolling views like {@link androidx.recyclerview.widget.RecyclerView}. * *

    You MUST implement {@link #onLoadCleared(Drawable)} and ensure that all references to * any resource passed into the target in {@link #onResourceReady(Object, Transition)} are removed * before {@link #onLoadCleared(Drawable)} completes. Failing to do so can result in graphical * corruption, crashes caused by recycled {@link Bitmap}s, and other undefined behavior. It is never * safe to leave {@link #onLoadCleared(Drawable)} unimplemented or empty. Even if you do not * manually clear this {@link Target}, Glide may do so automatically after certain lifecycle events * in {@link androidx.fragment.app.Fragment}s and {@link android.app.Activity}s. * *

    This class can only be used with {@link Target#SIZE_ORIGINAL} or when the desired resource * dimensions are known when the {@link Target} is created. If you'd like to run some asynchronous * process and make full use of {@link #getSize(SizeReadyCallback)} and {@link SizeReadyCallback}, * extend {@link Target} directly instead of using this class. * * @param The type of resource that will be loaded (e.g. {@link Bitmap}). */ public abstract class CustomTarget implements Target { private final int width; private final int height; @Nullable private Request request; /** * Creates a new {@link CustomTarget} that will attempt to load the resource in its original size. * *

    This constructor can cause very memory inefficient loads if the resource is large and can * cause OOMs. It's provided as a convenience for when you'd like to specify dimensions with * {@link com.bumptech.glide.request.RequestOptions#override(int)}. In all other cases, prefer * {@link #CustomTarget(int, int)}. */ public CustomTarget() { this(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); } /** * Creates a new {@code CustomTarget} that will return the given {@code width} and {@code height} * as the requested size (unless overridden by {@link * com.bumptech.glide.request.RequestOptions#override(int)} in the request). * * @param width The requested width in pixels ({@code > 0, or == Target.SIZE_ORIGINAL}). * @param height The requested height in pixels ({@code > 0, or == Target.SIZE_ORIGINAL}). * @throws IllegalArgumentException if width/height doesn't meet the requirement: {@code > 0, or * == Target.SIZE_ORIGINAL} */ public CustomTarget(int width, int height) { if (!Util.isValidDimensions(width, height)) { throw new IllegalArgumentException( "Width and height must both be > 0 or Target#SIZE_ORIGINAL, but given" + " width: " + width + " and height: " + height); } this.width = width; this.height = height; } @Override public void onStart() { // Intentionally empty, this can be optionally implemented by subclasses. } @Override public void onStop() { // Intentionally empty, this can be optionally implemented by subclasses. } @Override public void onDestroy() { // Intentionally empty, this can be optionally implemented by subclasses. } @Override public void onLoadStarted(@Nullable Drawable placeholder) { // Intentionally empty, this can be optionally implemented by subclasses. } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { // Intentionally empty, this can be optionally implemented by subclasses. } @Override public final void getSize(@NonNull SizeReadyCallback cb) { cb.onSizeReady(width, height); } @Override public final void removeCallback(@NonNull SizeReadyCallback cb) { // Do nothing, this class does not retain SizeReadyCallbacks. } @Override public final void setRequest(@Nullable Request request) { this.request = request; } @Nullable @Override public final Request getRequest() { return request; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/CustomViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.content.Context; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.Display; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.WindowManager; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.R; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * A base {@link Target} for loading resources ({@link android.graphics.Bitmap}, {@link Drawable} * etc) into {@link View}s that provides default implementations for most methods and can determine * the size of views using a {@link android.view.ViewTreeObserver.OnDrawListener}. * * @param The specific subclass of view wrapped by this target (e.g. {@link * android.widget.ImageView}) * @param The resource type this target will receive (e.g. {@link android.graphics.Bitmap}). */ public abstract class CustomViewTarget implements Target { private static final String TAG = "CustomViewTarget"; @IdRes private static final int VIEW_TAG_ID = R.id.glide_custom_view_target_tag; private final SizeDeterminer sizeDeterminer; protected final T view; @Nullable private OnAttachStateChangeListener attachStateListener; private boolean isClearedByUs; private boolean isAttachStateListenerAdded; /** Constructor that defaults {@code waitForLayout} to {@code false}. */ public CustomViewTarget(@NonNull T view) { this.view = Preconditions.checkNotNull(view); sizeDeterminer = new SizeDeterminer(view); } /** * A required callback invoked when the resource is no longer valid and must be freed. * *

    You must ensure that any current Drawable received in {@link #onResourceReady(Object, * Transition)} is no longer used before redrawing the container (usually a View) or changing its * visibility. Not doing so will result in crashes in your app. * * @param placeholder The placeholder drawable to optionally show, or null. */ protected abstract void onResourceCleared(@Nullable Drawable placeholder); /** * An optional callback invoked when a resource load is started. * * @see Target#onLoadStarted(Drawable) * @param placeholder The placeholder drawable to optionally show, or null. */ protected void onResourceLoading(@Nullable Drawable placeholder) { // Default empty. } @Override public void onStart() { // Default empty. } @Override public void onStop() { // Default empty. } @Override public void onDestroy() { // Default empty. } /** * Indicates that Glide should always wait for any pending layout pass before checking for the * size an {@link View}. * *

    By default, Glide will only wait for a pending layout pass if it's unable to resolve the * size from the {@link LayoutParams} or valid non-zero values for {@link View#getWidth()} and * {@link View#getHeight()}. * *

    Because calling this method forces Glide to wait for the layout pass to occur before * starting loads, setting this parameter to {@code true} can cause Glide to asynchronous load an * image even if it's in the memory cache. The load will happen asynchronously because Glide has * to wait for a layout pass to occur, which won't necessarily happen in the same frame as when * the image is requested. As a result, using this method can resulting in flashing in some cases * and should be used sparingly. * *

    If the {@link LayoutParams} of the wrapped {@link View} are set to fixed sizes, they will * still be used instead of the {@link View}'s dimensions even if this method is called. This * parameter is a fallback only. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull public final CustomViewTarget waitForLayout() { sizeDeterminer.waitForLayout = true; return this; } /** * Clears the {@link View}'s {@link Request} when the {@link View} is detached from its {@link * android.view.Window} and restarts the {@link Request} when the {@link View} is re-attached from * its {@link android.view.Window}. * *

    This is an experimental API that may be removed in a future version. * *

    Using this method can save memory by allowing Glide to more eagerly clear resources when * transitioning screens or swapping adapters in scrolling views. However it also substantially * increases the odds that images will not be in memory if users subsequently return to a screen * where images were previously loaded. Whether or not this happens will depend on the number of * images loaded in the new screen and the size of the memory cache. Increasing the size of the * memory cache can improve this behavior but it largely negates the memory benefits of using this * method. * *

    Use this method with caution and measure your memory usage to ensure that it's actually * improving your memory usage in the cases you care about. */ // Public API. @NonNull @SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) public final CustomViewTarget clearOnDetach() { if (attachStateListener != null) { return this; } attachStateListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { resumeMyRequest(); } @Override public void onViewDetachedFromWindow(View v) { pauseMyRequest(); } }; maybeAddAttachStateListener(); return this; } /** * Override the android resource id to store temporary state allowing loads to be automatically * cancelled and resources re-used in scrolling lists. * *

    Unlike {@link ViewTarget}, it is not necessary to set a custom tag id if your app * uses {@link View#setTag(Object)}. It is only necessary if loading several Glide resources into * the same view, for example one foreground and one background view. * * @param tagId The android resource id to use. * @deprecated Using this method prevents clearing the target from working properly. Glide uses * its own internal tag id so this method should not be necessary. This method is currently a * no-op. */ // Public API. @SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) @Deprecated public final CustomViewTarget useTagId(@IdRes int tagId) { return this; } /** Returns the wrapped {@link android.view.View}. */ @NonNull public final T getView() { return view; } /** * Determines the size of the view by first checking {@link android.view.View#getWidth()} and * {@link android.view.View#getHeight()}. If one or both are zero, it then checks the view's * {@link LayoutParams}. If one or both of the params width and height are less than or equal to * zero, it then adds an {@link android.view.ViewTreeObserver.OnPreDrawListener} which waits until * the view has been measured before calling the callback with the view's drawn width and height. * * @param cb {@inheritDoc} */ @Override public final void getSize(@NonNull SizeReadyCallback cb) { sizeDeterminer.getSize(cb); } @Override public final void removeCallback(@NonNull SizeReadyCallback cb) { sizeDeterminer.removeCallback(cb); } @Override public final void onLoadStarted(@Nullable Drawable placeholder) { maybeAddAttachStateListener(); onResourceLoading(placeholder); } @Override public final void onLoadCleared(@Nullable Drawable placeholder) { sizeDeterminer.clearCallbacksAndListener(); onResourceCleared(placeholder); if (!isClearedByUs) { maybeRemoveAttachStateListener(); } } /** * Stores the request using {@link View#setTag(Object)}. * * @param request {@inheritDoc} */ @Override public final void setRequest(@Nullable Request request) { setTag(request); } /** Returns any stored request using {@link android.view.View#getTag()}. */ @Override @Nullable public final Request getRequest() { Object tag = getTag(); if (tag != null) { if (tag instanceof Request) { return (Request) tag; } else { throw new IllegalArgumentException("You must not pass non-R.id ids to setTag(id)"); } } return null; } @Override public String toString() { return "Target for: " + view; } @SuppressWarnings("WeakerAccess") @Synthetic final void resumeMyRequest() { Request request = getRequest(); if (request != null && request.isCleared()) { request.begin(); } } @SuppressWarnings("WeakerAccess") @Synthetic final void pauseMyRequest() { Request request = getRequest(); if (request != null) { isClearedByUs = true; request.clear(); isClearedByUs = false; } } private void setTag(@Nullable Object tag) { view.setTag(VIEW_TAG_ID, tag); } @Nullable private Object getTag() { return view.getTag(VIEW_TAG_ID); } private void maybeAddAttachStateListener() { if (attachStateListener == null || isAttachStateListenerAdded) { return; } view.addOnAttachStateChangeListener(attachStateListener); isAttachStateListenerAdded = true; } private void maybeRemoveAttachStateListener() { if (attachStateListener == null || !isAttachStateListenerAdded) { return; } view.removeOnAttachStateChangeListener(attachStateListener); isAttachStateListenerAdded = false; } @VisibleForTesting static final class SizeDeterminer { // Some negative sizes (Target.SIZE_ORIGINAL) are valid, 0 is never valid. private static final int PENDING_SIZE = 0; @VisibleForTesting @Nullable static Integer maxDisplayLength; private final View view; private final List cbs = new ArrayList<>(); @Synthetic boolean waitForLayout; @Nullable private SizeDeterminerLayoutListener layoutListener; SizeDeterminer(@NonNull View view) { this.view = view; } // Use the maximum to avoid depending on the device's current orientation. private static int getMaxDisplayLength(@NonNull Context context) { if (maxDisplayLength == null) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = Preconditions.checkNotNull(windowManager).getDefaultDisplay(); Point displayDimensions = new Point(); display.getSize(displayDimensions); maxDisplayLength = Math.max(displayDimensions.x, displayDimensions.y); } return maxDisplayLength; } private void notifyCbs(int width, int height) { // One or more callbacks may trigger the removal of one or more additional callbacks, so we // need a copy of the list to avoid a concurrent modification exception. One place this // happens is when a full request completes from the in memory cache while its thumbnail is // still being loaded asynchronously. See #2237. for (SizeReadyCallback cb : new ArrayList<>(cbs)) { cb.onSizeReady(width, height); } } @Synthetic void checkCurrentDimens() { if (cbs.isEmpty()) { return; } int currentWidth = getTargetWidth(); int currentHeight = getTargetHeight(); if (!isViewStateAndSizeValid(currentWidth, currentHeight)) { return; } notifyCbs(currentWidth, currentHeight); clearCallbacksAndListener(); } void getSize(@NonNull SizeReadyCallback cb) { int currentWidth = getTargetWidth(); int currentHeight = getTargetHeight(); if (isViewStateAndSizeValid(currentWidth, currentHeight)) { cb.onSizeReady(currentWidth, currentHeight); return; } // We want to notify callbacks in the order they were added and we only expect one or two // callbacks to be added a time, so a List is a reasonable choice. if (!cbs.contains(cb)) { cbs.add(cb); } if (layoutListener == null) { ViewTreeObserver observer = view.getViewTreeObserver(); layoutListener = new SizeDeterminerLayoutListener(this); observer.addOnPreDrawListener(layoutListener); } } /** * The callback may be called anyway if it is removed by another {@link SizeReadyCallback} or * otherwise removed while we're notifying the list of callbacks. * *

    See #2237. */ void removeCallback(@NonNull SizeReadyCallback cb) { cbs.remove(cb); } void clearCallbacksAndListener() { // Keep a reference to the layout attachStateListener and remove it here // rather than having the observer remove itself because the observer // we add the attachStateListener to will be almost immediately merged into // another observer and will therefore never be alive. If we instead // keep a reference to the attachStateListener and remove it here, we get the // current view tree observer and should succeed. ViewTreeObserver observer = view.getViewTreeObserver(); if (observer.isAlive()) { observer.removeOnPreDrawListener(layoutListener); } layoutListener = null; cbs.clear(); } private boolean isViewStateAndSizeValid(int width, int height) { return isDimensionValid(width) && isDimensionValid(height); } private int getTargetHeight() { int verticalPadding = view.getPaddingTop() + view.getPaddingBottom(); LayoutParams layoutParams = view.getLayoutParams(); int layoutParamSize = layoutParams != null ? layoutParams.height : PENDING_SIZE; return getTargetDimen(view.getHeight(), layoutParamSize, verticalPadding); } private int getTargetWidth() { int horizontalPadding = view.getPaddingLeft() + view.getPaddingRight(); LayoutParams layoutParams = view.getLayoutParams(); int layoutParamSize = layoutParams != null ? layoutParams.width : PENDING_SIZE; return getTargetDimen(view.getWidth(), layoutParamSize, horizontalPadding); } private int getTargetDimen(int viewSize, int paramSize, int paddingSize) { // We consider the View state as valid if the View has non-null layout params and a non-zero // layout params width and height. This is imperfect. We're making an assumption that View // parents will obey their child's layout parameters, which isn't always the case. int adjustedParamSize = paramSize - paddingSize; if (adjustedParamSize > 0) { return adjustedParamSize; } // Since we always prefer layout parameters with fixed sizes, even if waitForLayout is true, // we might as well ignore it and just return the layout parameters above if we have them. // Otherwise we should wait for a layout pass before checking the View's dimensions. if (waitForLayout && view.isLayoutRequested()) { return PENDING_SIZE; } // We also consider the View state valid if the View has a non-zero width and height. This // means that the View has gone through at least one layout pass. It does not mean the Views // width and height are from the current layout pass. For example, if a View is re-used in // RecyclerView or ListView, this width/height may be from an old position. In some cases // the dimensions of the View at the old position may be different than the dimensions of the // View in the new position because the LayoutManager/ViewParent can arbitrarily decide to // change them. Nevertheless, in most cases this should be a reasonable choice. int adjustedViewSize = viewSize - paddingSize; if (adjustedViewSize > 0) { return adjustedViewSize; } // Finally we consider the view valid if the layout parameter size is set to wrap_content. // It's difficult for Glide to figure out what to do here. Although Target.SIZE_ORIGINAL is a // coherent choice, it's extremely dangerous because original images may be much too large to // fit in memory or so large that only a couple can fit in memory, causing OOMs. If users want // the original image, they can always use .override(Target.SIZE_ORIGINAL). Since wrap_content // may never resolve to a real size unless we load something, we aim for a square whose length // is the largest screen size. That way we're loading something and that something has some // hope of being downsampled to a size that the device can support. We also log a warning that // tries to explain what Glide is doing and why some alternatives are preferable. // Since WRAP_CONTENT is sometimes used as a default layout parameter, we always wait for // layout to complete before using this fallback parameter (ConstraintLayout among others). if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) { if (Log.isLoggable(TAG, Log.INFO)) { Log.i( TAG, "Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of" + " this device's screen dimensions. If you want to load the original image and" + " are ok with the corresponding memory cost and OOMs (depending on the input" + " size), use .override(Target.SIZE_ORIGINAL). Otherwise, use" + " LayoutParams.MATCH_PARENT, set layout_width and layout_height to fixed" + " dimension, or use .override() with fixed dimensions."); } return getMaxDisplayLength(view.getContext()); } // If the layout parameters are < padding, the view size is < padding, or the layout // parameters are set to match_parent or wrap_content and no layout has occurred, we should // wait for layout and repeat. return PENDING_SIZE; } private boolean isDimensionValid(int size) { return size > 0 || size == SIZE_ORIGINAL; } private static final class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener { private final WeakReference sizeDeterminerRef; SizeDeterminerLayoutListener(@NonNull SizeDeterminer sizeDeterminer) { sizeDeterminerRef = new WeakReference<>(sizeDeterminer); } @Override public boolean onPreDraw() { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "OnGlobalLayoutListener called attachStateListener=" + this); } SizeDeterminer sizeDeterminer = sizeDeterminerRef.get(); if (sizeDeterminer != null) { sizeDeterminer.checkCurrentDimens(); } return true; } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/DrawableImageViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.annotation.Nullable; /** A target for display {@link Drawable} objects in {@link ImageView}s. */ public class DrawableImageViewTarget extends ImageViewTarget { public DrawableImageViewTarget(ImageView view) { super(view); } /** * @deprecated Use {@link #waitForLayout()} instead. */ // Public API. @SuppressWarnings({"unused", "deprecation"}) @Deprecated public DrawableImageViewTarget(ImageView view, boolean waitForLayout) { super(view, waitForLayout); } @Override protected void setResource(@Nullable Drawable resource) { view.setImageDrawable(resource); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/DrawableThumbnailImageViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Drawable; import android.widget.ImageView; /** * Efficiently displays multiple Drawables loaded serially into a single {@link android.view.View}. */ // Public API. @SuppressWarnings("unused") public class DrawableThumbnailImageViewTarget extends ThumbnailImageViewTarget { public DrawableThumbnailImageViewTarget(ImageView view) { super(view); } /** * @deprecated Use {@link #waitForLayout()} instead. */ @Deprecated @SuppressWarnings("deprecation") public DrawableThumbnailImageViewTarget(ImageView view, boolean waitForLayout) { super(view, waitForLayout); } @Override protected Drawable getDrawable(Drawable resource) { return resource; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/FixedSizeDrawable.java ================================================ package com.bumptech.glide.request.target; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; /** * A wrapper drawable to square the wrapped drawable so that it expands to fill a square with * exactly the given side length. The goal of this drawable is to ensure that square thumbnail * drawables always match the size of the view they will be displayed in to avoid a costly * requestLayout call. This class should not be used with views or drawables that are not square. */ public class FixedSizeDrawable extends Drawable { private final Matrix matrix; private final RectF wrappedRect; private final RectF bounds; private Drawable wrapped; private State state; private boolean mutated; // Public API. @SuppressWarnings("WeakerAccess") public FixedSizeDrawable(Drawable wrapped, int width, int height) { this(new State(wrapped.getConstantState(), width, height), wrapped); } FixedSizeDrawable(State state, Drawable wrapped) { this.state = Preconditions.checkNotNull(state); this.wrapped = Preconditions.checkNotNull(wrapped); // We will do our own scaling. wrapped.setBounds(0, 0, wrapped.getIntrinsicWidth(), wrapped.getIntrinsicHeight()); matrix = new Matrix(); wrappedRect = new RectF(0, 0, wrapped.getIntrinsicWidth(), wrapped.getIntrinsicHeight()); bounds = new RectF(); } @Override public void setBounds(int left, int top, int right, int bottom) { super.setBounds(left, top, right, bottom); bounds.set(left, top, right, bottom); updateMatrix(); } @Override public void setBounds(@NonNull Rect bounds) { super.setBounds(bounds); this.bounds.set(bounds); updateMatrix(); } private void updateMatrix() { matrix.setRectToRect(wrappedRect, this.bounds, Matrix.ScaleToFit.CENTER); } @Override public void setChangingConfigurations(int configs) { wrapped.setChangingConfigurations(configs); } @Override public int getChangingConfigurations() { return wrapped.getChangingConfigurations(); } @Deprecated @Override public void setDither(boolean dither) { wrapped.setDither(dither); } @Override public void setFilterBitmap(boolean filter) { wrapped.setFilterBitmap(filter); } @Override public Callback getCallback() { return wrapped.getCallback(); } @RequiresApi(Build.VERSION_CODES.KITKAT) @Override public int getAlpha() { return wrapped.getAlpha(); } @Override public void setColorFilter(int color, @NonNull PorterDuff.Mode mode) { wrapped.setColorFilter(color, mode); } @Override public void clearColorFilter() { wrapped.clearColorFilter(); } @NonNull @Override public Drawable getCurrent() { return wrapped.getCurrent(); } @Override public boolean setVisible(boolean visible, boolean restart) { return wrapped.setVisible(visible, restart); } @Override public int getIntrinsicWidth() { return state.width; } @Override public int getIntrinsicHeight() { return state.height; } @Override public int getMinimumWidth() { return wrapped.getMinimumWidth(); } @Override public int getMinimumHeight() { return wrapped.getMinimumHeight(); } @Override public boolean getPadding(@NonNull Rect padding) { return wrapped.getPadding(padding); } @Override public void invalidateSelf() { super.invalidateSelf(); wrapped.invalidateSelf(); } @Override public void unscheduleSelf(@NonNull Runnable what) { super.unscheduleSelf(what); wrapped.unscheduleSelf(what); } @Override public void scheduleSelf(@NonNull Runnable what, long when) { super.scheduleSelf(what, when); wrapped.scheduleSelf(what, when); } @Override public void draw(@NonNull Canvas canvas) { canvas.save(); canvas.concat(matrix); wrapped.draw(canvas); canvas.restore(); } @Override public void setAlpha(int i) { wrapped.setAlpha(i); } @Override public void setColorFilter(ColorFilter colorFilter) { wrapped.setColorFilter(colorFilter); } @Override public int getOpacity() { return wrapped.getOpacity(); } @NonNull @Override public Drawable mutate() { if (!mutated && super.mutate() == this) { wrapped = wrapped.mutate(); state = new State(state); mutated = true; } return this; } @Override public ConstantState getConstantState() { return state; } static final class State extends ConstantState { private final ConstantState wrapped; @Synthetic final int width; @Synthetic final int height; State(State other) { this(other.wrapped, other.width, other.height); } State(ConstantState wrapped, int width, int height) { this.wrapped = wrapped; this.width = width; this.height = height; } @NonNull @Override public Drawable newDrawable() { return new FixedSizeDrawable(this, wrapped.newDrawable()); } @NonNull @Override public Drawable newDrawable(Resources res) { return new FixedSizeDrawable(this, wrapped.newDrawable(res)); } @Override public int getChangingConfigurations() { return 0; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/ImageViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.request.transition.Transition; /** * A base {@link com.bumptech.glide.request.target.Target} for displaying resources in {@link * android.widget.ImageView}s. * * @param The type of resource that this target will display in the wrapped {@link * android.widget.ImageView}. */ // Public API. @SuppressWarnings("WeakerAccess") public abstract class ImageViewTarget extends ViewTarget implements Transition.ViewAdapter { @Nullable private Animatable animatable; public ImageViewTarget(ImageView view) { super(view); } /** * @deprecated Use {@link #waitForLayout()} instead. */ @SuppressWarnings({"deprecation"}) @Deprecated public ImageViewTarget(ImageView view, boolean waitForLayout) { super(view, waitForLayout); } /** * Returns the current {@link android.graphics.drawable.Drawable} being displayed in the view * using {@link android.widget.ImageView#getDrawable()}. */ @Override @Nullable public Drawable getCurrentDrawable() { return view.getDrawable(); } /** * Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link * android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. * * @param drawable {@inheritDoc} */ @Override public void setDrawable(Drawable drawable) { view.setImageDrawable(drawable); } /** * Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link * android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. * * @param placeholder {@inheritDoc} */ @Override public void onLoadStarted(@Nullable Drawable placeholder) { super.onLoadStarted(placeholder); setResourceInternal(null); setDrawable(placeholder); } /** * Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link * android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. * * @param errorDrawable {@inheritDoc} */ @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { super.onLoadFailed(errorDrawable); setResourceInternal(null); setDrawable(errorDrawable); } /** * Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link * android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. * * @param placeholder {@inheritDoc} */ @Override public void onLoadCleared(@Nullable Drawable placeholder) { super.onLoadCleared(placeholder); if (animatable != null) { animatable.stop(); } setResourceInternal(null); setDrawable(placeholder); } @Override public void onResourceReady(@NonNull Z resource, @Nullable Transition transition) { if (transition == null || !transition.transition(resource, this)) { setResourceInternal(resource); } else { maybeUpdateAnimatable(resource); } } @Override public void onStart() { if (animatable != null) { animatable.start(); } } @Override public void onStop() { if (animatable != null) { animatable.stop(); } } private void setResourceInternal(@Nullable Z resource) { // Order matters here. Set the resource first to make sure that the Drawable has a valid and // non-null Callback before starting it. setResource(resource); maybeUpdateAnimatable(resource); } private void maybeUpdateAnimatable(@Nullable Z resource) { if (resource instanceof Animatable) { animatable = (Animatable) resource; animatable.start(); } else { animatable = null; } } protected abstract void setResource(@Nullable Z resource); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/ImageViewTargetFactory.java ================================================ package com.bumptech.glide.request.target; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.annotation.NonNull; /** * A factory responsible for producing the correct type of {@link * com.bumptech.glide.request.target.Target} for a given {@link android.view.View} subclass. */ public class ImageViewTargetFactory { @NonNull @SuppressWarnings("unchecked") public ViewTarget buildTarget( @NonNull ImageView view, @NonNull Class clazz) { if (Bitmap.class.equals(clazz)) { return (ViewTarget) new BitmapImageViewTarget(view); } else if (Drawable.class.isAssignableFrom(clazz)) { return (ViewTarget) new DrawableImageViewTarget(view); } else { throw new IllegalArgumentException( "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)"); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/NotificationTarget.java ================================================ package com.bumptech.glide.request.target; import android.Manifest; import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Preconditions; /** * This class is used to display downloaded Bitmap inside an ImageView of a Notification through * RemoteViews. * *

    Note - For cancellation to work correctly, you must pass in the same instance of this class * for every subsequent load. */ // Public API. @SuppressWarnings({"WeakerAccess", "unused"}) public class NotificationTarget extends CustomTarget { private final RemoteViews remoteViews; private final Context context; private final int notificationId; private final String notificationTag; private final Notification notification; private final int viewId; /** * Constructor using a Notification object and a notificationId to get a handle on the * Notification in order to update it that uses {@link #SIZE_ORIGINAL} as the target width and * height. * * @param context Context to use in the AppWidgetManager initialization. * @param viewId The id of the ImageView view that will load the image. * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. * @param notification The Notification object that we want to update. * @param notificationId The notificationId of the Notification that we want to load the Bitmap. */ @SuppressLint("InlinedApi") // Alert users of Glide to have this permission. @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) public NotificationTarget( Context context, int viewId, RemoteViews remoteViews, Notification notification, int notificationId) { this(context, viewId, remoteViews, notification, notificationId, null); } /** * Constructor using a Notification object, a notificationId, and a notificationTag to get a * handle on the Notification in order to update it that uses {@link #SIZE_ORIGINAL} as the target * width and height. * * @param context Context to use in the AppWidgetManager initialization. * @param viewId The id of the ImageView view that will load the image. * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. * @param notification The Notification object that we want to update. * @param notificationId The notificationId of the Notification that we want to load the Bitmap. * @param notificationTag The notificationTag of the Notification that we want to load the Bitmap. * May be {@code null}. */ @SuppressLint("InlinedApi") // Alert users of Glide to have this permission. @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) public NotificationTarget( Context context, int viewId, RemoteViews remoteViews, Notification notification, int notificationId, String notificationTag) { this( context, SIZE_ORIGINAL, SIZE_ORIGINAL, viewId, remoteViews, notification, notificationId, notificationTag); } /** * Constructor using a Notification object, a notificationId, and a notificationTag to get a * handle on the Notification in order to update it. * * @param context Context to use in the AppWidgetManager initialization. * @param width Desired width of the bitmap that will be loaded.(Need to be manually put because * of RemoteViews limitations.) * @param height Desired height of the bitmap that will be loaded. (Need to be manually put * because of RemoteViews limitations.) * @param viewId The id of the ImageView view that will load the image. * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. * @param notification The Notification object that we want to update. * @param notificationId The notificationId of the Notification that we want to load the Bitmap. * @param notificationTag The notificationTag of the Notification that we want to load the Bitmap. * May be {@code null}. */ @SuppressLint("InlinedApi") // Alert users of Glide to have this permission. @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) public NotificationTarget( Context context, int width, int height, int viewId, RemoteViews remoteViews, Notification notification, int notificationId, String notificationTag) { super(width, height); this.context = Preconditions.checkNotNull(context, "Context must not be null!"); this.notification = Preconditions.checkNotNull(notification, "Notification object can not be null!"); this.remoteViews = Preconditions.checkNotNull(remoteViews, "RemoteViews object can not be null!"); this.viewId = viewId; this.notificationId = notificationId; this.notificationTag = notificationTag; } /** Updates the Notification after the Bitmap resource is loaded. */ @SuppressLint("InlinedApi") // Help tools to recognize that this method requires a permission, because it posts a // notification. @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) private void update() { NotificationManager manager = (NotificationManager) this.context.getSystemService(Context.NOTIFICATION_SERVICE); Preconditions.checkNotNull(manager) .notify(this.notificationTag, this.notificationId, this.notification); } @SuppressLint("InlinedApi") // Help tools to recognize that this method requires a permission, because it calls setBitmap(). @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) @Override public void onResourceReady( @NonNull Bitmap resource, @Nullable Transition transition) { setBitmap(resource); } @SuppressLint("InlinedApi") // Help tools to recognize that this method requires a permission, because it calls setBitmap(). @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) @Override public void onLoadCleared(@Nullable Drawable placeholder) { setBitmap(null); } @SuppressLint("InlinedApi") // Help tools to recognize that this method requires a permission, because it calls update(). @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) private void setBitmap(@Nullable Bitmap bitmap) { this.remoteViews.setImageViewBitmap(this.viewId, bitmap); this.update(); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/PreloadTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Synthetic; /** * A one time use {@link com.bumptech.glide.request.target.Target} class that loads a resource into * memory and then clears itself. * * @param The type of resource that will be loaded into memory. */ public final class PreloadTarget extends CustomTarget { private static final int MESSAGE_CLEAR = 1; private static final Handler HANDLER = new Handler( Looper.getMainLooper(), new Callback() { @Override public boolean handleMessage(Message message) { if (message.what == MESSAGE_CLEAR) { ((PreloadTarget) message.obj).clear(); return true; } return false; } }); private final RequestManager requestManager; /** * Returns a PreloadTarget. * * @param width The width in pixels of the desired resource. * @param height The height in pixels of the desired resource. * @param The type of the desired resource. */ public static PreloadTarget obtain(RequestManager requestManager, int width, int height) { return new PreloadTarget<>(requestManager, width, height); } private PreloadTarget(RequestManager requestManager, int width, int height) { super(width, height); this.requestManager = requestManager; } @Override public void onResourceReady(@NonNull Z resource, @Nullable Transition transition) { // If a thumbnail request is set and the thumbnail completes, we don't want to cancel the // primary load. Instead we wait until the primary request (the one set on the target) says // that it is complete. // Note - Any thumbnail request that does not complete before the primary request will be // cancelled and may not be preloaded successfully. Cancellation of outstanding thumbnails after // the primary request succeeds is a common behavior of all Glide requests and we're not trying // to override it here. Request request = getRequest(); if (request != null && request.isComplete()) { HANDLER.obtainMessage(MESSAGE_CLEAR, this).sendToTarget(); } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { // Do nothing, we don't retain a reference to our resource. } @SuppressWarnings("WeakerAccess") @Synthetic void clear() { requestManager.clear(this); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/SimpleTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Drawable; import android.view.View; import androidx.annotation.NonNull; import com.bumptech.glide.util.Util; /** * A simple {@link com.bumptech.glide.request.target.Target} base class with default (usually no-op) * implementations of non essential methods that allows the caller to specify an exact width/height. * Typically use cases look something like this: * *

    {@code
     * Target target =
     *     Glide.with(fragment)
     *       .asBitmap()
     *       .load("http://somefakeurl.com/fakeImage.jpeg")
     *       .apply(fitCenterTransform())
     *       .into(new SimpleTarget(250, 250) {
     *
     *         {@literal @Override}
     *         public void onResourceReady(Bitmap resource, Transition transition) {
     *           // Do something with bitmap here.
     *         }
     *
     *       });
     * }
     * // At some later point, clear the Target to release the resources, prevent load queues from
     * // blowing out proportion, and to improve load times for future requests:
     * Glide.with(fragment).clear(target);
     * }
    * *

    Warning! this class is extremely prone to mis-use. Use SimpleTarget only as a last * resort. {@link ViewTarget} or a subclass of {@link ViewTarget} is almost always a better choice. * *

    Don't forget to clear instances of this class!. If you must use this class, keep in * mind that unlike {@link ViewTarget} it is not safe to load into new instances of this class * repeatedly if every instance updates the same underlying {@link View} or caller. If you need to * load into the same {@link View} or caller repeatedly using this class, always retain a reference * to the previous instance and either call {@link com.bumptech.glide.RequestManager#clear(Target)} * on the old instance before starting a new load or you must re-use the old instance for the new * load. Glide's {@link com.bumptech.glide.RequestBuilder#into(Target)} method returns the {@link * Target} instance you provided to make retaining a reference to the {@link Target} as easy as * possible. That said, you must wait until you're completely finished with the resource before * calling {@link com.bumptech.glide.RequestManager#clear(Target)} and you should always null out * references to any loaded resources in {@link Target#onLoadCleared(Drawable)}. * *

    Always try to provide a size when using this class. Use {@link SimpleTarget#SimpleTarget(int, * int)} whenever possible with values that are not {@link Target#SIZE_ORIGINAL}. Using * {@link Target#SIZE_ORIGINAL} is unsafe if you're loading large images or are running your * application on older or memory constrained devices because it can cause Glide to load very large * images into memory. In some cases those images may throw {@link OutOfMemoryError} and in others * they may exceed the texture limit for the device, which will prevent them from being rendered. * Providing a valid size allows Glide to downsample large images, which can avoid issues with * texture size or memory limitations. You don't have to worry about providing a size in most cases * if you use {@link ViewTarget} so prefer {@link ViewTarget} over this class whenver possible. * * @see Glide's Target docs page * @param The type of resource that this target will receive. * @deprecated Use {@link CustomViewTarget} if loading the content into a view, the download API if * in the background * (http://bumptech.github.io/glide/doc/getting-started.html#background-threads), or a {@link * CustomTarget} for any specialized use-cases. Using {@link SimpleTarget} or {@link BaseTarget} * is unsafe if the user does not implement {@link #onLoadCleared}, resulting in recycled * bitmaps being referenced from the UI and hard to root-cause crashes. */ @Deprecated public abstract class SimpleTarget extends BaseTarget { private final int width; private final int height; /** * Constructor for the target that uses {@link Target#SIZE_ORIGINAL} as the target width and * height. */ // Public API. @SuppressWarnings("WeakerAccess") public SimpleTarget() { this(SIZE_ORIGINAL, SIZE_ORIGINAL); } /** * Constructor for the target that takes the desired dimensions of the decoded and/or transformed * resource. * * @param width The width in pixels of the desired resource. * @param height The height in pixels of the desired resource. */ // Public API. @SuppressWarnings("WeakerAccess") public SimpleTarget(int width, int height) { this.width = width; this.height = height; } /** * Immediately calls the given callback with the sizes given in the constructor. * * @param cb {@inheritDoc} */ @Override public final void getSize(@NonNull SizeReadyCallback cb) { if (!Util.isValidDimensions(width, height)) { throw new IllegalArgumentException( "Width and height must both be > 0 or Target#SIZE_ORIGINAL, but given" + " width: " + width + " and height: " + height + ", either provide dimensions in the constructor" + " or call override()"); } cb.onSizeReady(width, height); } @Override public void removeCallback(@NonNull SizeReadyCallback cb) { // Do nothing, we never retain a reference to the callback. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/SizeReadyCallback.java ================================================ package com.bumptech.glide.request.target; /** * A callback that must be called when the target has determined its size. For fixed size targets it * can be called synchronously. */ public interface SizeReadyCallback { /** * A callback called on the main thread. * * @param width The width in pixels of the target, or {@link Target#SIZE_ORIGINAL} to indicate * that we want the resource at its original width. * @param height The height in pixels of the target, or {@link Target#SIZE_ORIGINAL} to indicate * that we want the resource at its original height. */ void onSizeReady(int width, int height); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/Target.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.manager.LifecycleListener; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.transition.Transition; /** * An interface that Glide can load a resource into and notify of relevant lifecycle events during a * load. * *

    The lifecycle events in this class are as follows: * *

      *
    • onLoadStarted *
    • onResourceReady *
    • onLoadCleared *
    • onLoadFailed *
    * *

    The typical lifecycle is onLoadStarted, then onResourceReady or onLoadFailed, then * onLoadCleared. However, there are no guarantees. onLoadStarted may not be called if the resource * is in memory or if the load will fail because of a null model object. onLoadCleared similarly may * never be called if the target is never cleared. See the docs for the individual methods for * details. * * @param The type of resource the target can display. */ public interface Target extends LifecycleListener { /** Indicates that we want the resource in its original unmodified width and/or height. */ int SIZE_ORIGINAL = Integer.MIN_VALUE; /** * A lifecycle callback that is called when a load is started. * *

    Note - This may not be called for every load, it is possible for example for loads to fail * before the load starts (when the model object is null). * *

    Note - This method may be called multiple times before any other lifecycle method is called. * Loads can be paused and restarted due to lifecycle or connectivity events and each restart may * cause a call here. * * @param placeholder The placeholder drawable to optionally show, or null. */ void onLoadStarted(@Nullable Drawable placeholder); /** * A mandatory lifecycle callback that is called when a load fails. * *

    Note - This may be called before {@link #onLoadStarted(android.graphics.drawable.Drawable) } * if the model object is null. * *

    You must ensure that any current Drawable received in {@link #onResourceReady(Object, * Transition)} is no longer used before redrawing the container (usually a View) or changing its * visibility. * * @param errorDrawable The error drawable to optionally show, or null. */ void onLoadFailed(@Nullable Drawable errorDrawable); /** * The method that will be called when the resource load has finished. * *

    This may be called multiple times both within a single load and also across different loads * if the {@code Target} object is re-used. * *

    Within a single load this may be called multiple times for reasons that include: * *

      *
    • The load uses one or more thumbnails. Each time a thumbnail load completes successfully * and no higher priority load has finished, this method will be called with the thumbnail * resource. See {@link * com.bumptech.glide.RequestBuilder#thumbnail(com.bumptech.glide.RequestBuilder)}. *
    • The load is paused and restarted. This can happen automatically in response to * connectivity changes or the Activity / Fragment lifecycle. It can also happen if {@link * com.bumptech.glide.RequestManager#pauseRequests()} is called manually. *
    * * @param resource the loaded resource. */ void onResourceReady(@NonNull R resource, @Nullable Transition transition); /** * A mandatory lifecycle callback that is called when a load is cancelled and its resources * are freed. * *

    You must ensure that any current Drawable received in {@link #onResourceReady(Object, * Transition)} is no longer used before redrawing the container (usually a View) or changing its * visibility. * * @param placeholder The placeholder drawable to optionally show, or null. */ void onLoadCleared(@Nullable Drawable placeholder); /** * A method to retrieve the size of this target. * * @param cb The callback that must be called when the size of the target has been determined */ void getSize(@NonNull SizeReadyCallback cb); /** * Removes the given callback from the pending set if it's still retained. * * @param cb The callback to remove. */ void removeCallback(@NonNull SizeReadyCallback cb); /** Sets the current request for this target to retain, should not be called outside of Glide. */ void setRequest(@Nullable Request request); /** Retrieves the current request for this target, should not be called outside of Glide. */ @Nullable Request getRequest(); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/ThumbnailImageViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.graphics.drawable.Drawable; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.Nullable; /** * Avoids extra calls to {@link android.view.View#requestLayout} when loading more than once image * into an {@link android.widget.ImageView} with fixed dimensions. * *

    Typically it makes sense to use this class when loading multiple images with the {@link * com.bumptech.glide.RequestBuilder#thumbnail(com.bumptech.glide.RequestBuilder)} API into views in * a scrolling list like ListView, GridView, or RecyclerView. * *

    {@link FixedSizeDrawable} may cause skewing or other undesirable behavior depending on your * images, views, and scaling. If this occurs, consider {@link DrawableImageViewTarget} or {@link * BitmapImageViewTarget} as alternatives. * * @param The type of resource that will be displayed in the ImageView. */ // Public API. @SuppressWarnings("WeakerAccess") public abstract class ThumbnailImageViewTarget extends ImageViewTarget { public ThumbnailImageViewTarget(ImageView view) { super(view); } /** * @deprecated Use {@link #waitForLayout()} insetad. */ @Deprecated @SuppressWarnings({"deprecation"}) public ThumbnailImageViewTarget(ImageView view, boolean waitForLayout) { super(view, waitForLayout); } @Override protected void setResource(@Nullable T resource) { ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); Drawable result = getDrawable(resource); if (layoutParams != null && layoutParams.width > 0 && layoutParams.height > 0) { result = new FixedSizeDrawable(result, layoutParams.width, layoutParams.height); } view.setImageDrawable(result); } protected abstract Drawable getDrawable(T resource); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/target/ViewTarget.java ================================================ package com.bumptech.glide.request.target; import android.content.Context; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.Display; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.WindowManager; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.R; import com.bumptech.glide.request.Request; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * A base {@link Target} for loading {@link android.graphics.Bitmap}s into {@link View}s that * provides default implementations for most most methods and can determine the size of views using * a {@link android.view.ViewTreeObserver.OnDrawListener}. * *

    To detect {@link View} reuse in {@link android.widget.ListView} or any {@link * android.view.ViewGroup} that reuses views, this class uses the {@link View#setTag(Object)} method * to store some metadata so that if a view is reused, any previous loads or resources from previous * loads can be cancelled or reused. * *

    Any calls to {@link View#setTag(Object)}} on a View given to this class will result in * excessive allocations and and/or {@link IllegalArgumentException}s. If you must call {@link * View#setTag(Object)} on a view, use {@link #setTagId(int)} to specify a custom tag for Glide to * use. * *

    Subclasses must call super in {@link #onLoadCleared(Drawable)} * * @param The specific subclass of view wrapped by this target. * @param The resource type this target will receive. * @deprecated Use {@link CustomViewTarget}. Using this class is unsafe without implementing {@link * #onLoadCleared} and results in recycled bitmaps being referenced from the UI and hard to * root-cause crashes. */ @Deprecated public abstract class ViewTarget extends BaseTarget { private static final String TAG = "ViewTarget"; private static boolean isTagUsedAtLeastOnce; private static int tagId = R.id.glide_custom_view_target_tag; protected final T view; private final SizeDeterminer sizeDeterminer; @Nullable private OnAttachStateChangeListener attachStateListener; private boolean isClearedByUs; private boolean isAttachStateListenerAdded; /** Constructor that defaults {@code waitForLayout} to {@code false}. */ public ViewTarget(@NonNull T view) { this.view = Preconditions.checkNotNull(view); sizeDeterminer = new SizeDeterminer(view); } /** * @param waitForLayout If set to {@code true}, Glide will always wait for any pending layout pass * before checking for the size a View. If set to {@code false} Glide will only wait for a * pending layout pass if it's unable to resolve the size from layout parameters or an * existing View size. Because setting this parameter to {@code true} forces Glide to wait for * the layout pass to occur before starting the load, setting this parameter to {@code true} * can cause flashing in some cases and should be used sparingly. If layout parameters are set * to fixed sizes, they will still be used instead of the View's dimensions even if this * parameter is set to {@code true}. This parameter is a fallback only. * @deprecated Use {@link #waitForLayout()} instead. */ @SuppressWarnings("WeakerAccess") // Public API @Deprecated public ViewTarget(@NonNull T view, boolean waitForLayout) { this(view); if (waitForLayout) { waitForLayout(); } } /** * Clears the {@link View}'s {@link Request} when the {@link View} is detached from its {@link * android.view.Window} and restarts the {@link Request} when the {@link View} is re-attached from * its {@link android.view.Window}. * *

    This is an experimental API that may be removed in a future version. * *

    Using this method can save memory by allowing Glide to more eagerly clear resources when * transitioning screens or swapping adapters in scrolling views. However it also substantially * increases the odds that images will not be in memory if users subsequently return to a screen * where images were previously loaded. Whether or not this happens will depend on the number of * images loaded in the new screen and the size of the memory cache. Increasing the size of the * memory cache can improve this behavior but it largely negates the memory benefits of using this * method. * *

    Use this method with caution and measure your memory usage to ensure that it's actually * improving your memory usage in the cases you care about. */ // Public API. @NonNull @SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) public final ViewTarget clearOnDetach() { if (attachStateListener != null) { return this; } attachStateListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { resumeMyRequest(); } @Override public void onViewDetachedFromWindow(View v) { pauseMyRequest(); } }; maybeAddAttachStateListener(); return this; } @SuppressWarnings("WeakerAccess") @Synthetic void resumeMyRequest() { Request request = getRequest(); if (request != null && request.isCleared()) { request.begin(); } } @SuppressWarnings("WeakerAccess") @Synthetic void pauseMyRequest() { Request request = getRequest(); // If the Request were cleared by the developer, it would be null here. The only way it's // present is if the developer hasn't previously cleared this Target. if (request != null) { isClearedByUs = true; request.clear(); isClearedByUs = false; } } /** * Indicates that Glide should always wait for any pending layout pass before checking for the * size an {@link View}. * *

    By default, Glide will only wait for a pending layout pass if it's unable to resolve the * size from the {@link LayoutParams} or valid non-zero values for {@link View#getWidth()} and * {@link View#getHeight()}. * *

    Because calling this method forces Glide to wait for the layout pass to occur before * starting loads, setting this parameter to {@code true} can cause Glide to asynchronous load an * image even if it's in the memory cache. The load will happen asynchronously because Glide has * to wait for a layout pass to occur, which won't necessarily happen in the same frame as when * the image is requested. As a result, using this method can resulting in flashing in some cases * and should be used sparingly. * *

    If the {@link LayoutParams} of the wrapped {@link View} are set to fixed sizes, they will * still be used instead of the {@link View}'s dimensions even if this method is called. This * parameter is a fallback only. */ @SuppressWarnings("WeakerAccess") // Public API @NonNull public final ViewTarget waitForLayout() { sizeDeterminer.waitForLayout = true; return this; } @CallSuper @Override public void onLoadStarted(@Nullable Drawable placeholder) { super.onLoadStarted(placeholder); maybeAddAttachStateListener(); } private void maybeAddAttachStateListener() { if (attachStateListener == null || isAttachStateListenerAdded) { return; } view.addOnAttachStateChangeListener(attachStateListener); isAttachStateListenerAdded = true; } private void maybeRemoveAttachStateListener() { if (attachStateListener == null || !isAttachStateListenerAdded) { return; } view.removeOnAttachStateChangeListener(attachStateListener); isAttachStateListenerAdded = false; } /** Returns the wrapped {@link android.view.View}. */ @NonNull public T getView() { return view; } /** * Determines the size of the view by first checking {@link android.view.View#getWidth()} and * {@link android.view.View#getHeight()}. If one or both are zero, it then checks the view's * {@link LayoutParams}. If one or both of the params width and height are less than or equal to * zero, it then adds an {@link android.view.ViewTreeObserver.OnPreDrawListener} which waits until * the view has been measured before calling the callback with the view's drawn width and height. * * @param cb {@inheritDoc} */ @CallSuper @Override public void getSize(@NonNull SizeReadyCallback cb) { sizeDeterminer.getSize(cb); } @CallSuper @Override public void removeCallback(@NonNull SizeReadyCallback cb) { sizeDeterminer.removeCallback(cb); } @CallSuper @Override public void onLoadCleared(@Nullable Drawable placeholder) { super.onLoadCleared(placeholder); sizeDeterminer.clearCallbacksAndListener(); if (!isClearedByUs) { maybeRemoveAttachStateListener(); } } /** * Stores the request using {@link View#setTag(Object)}. * * @param request {@inheritDoc} */ @Override public void setRequest(@Nullable Request request) { setTag(request); } /** * Returns any stored request using {@link android.view.View#getTag()}. * *

    For Glide to function correctly, Glide must be the only thing that calls {@link * View#setTag(Object)}. If the tag is cleared or put to another object type, Glide will not be * able to retrieve and cancel previous loads which will not only prevent Glide from reusing * resource, but will also result in incorrect images being loaded and lots of flashing of images * in lists. As a result, this will throw an {@link java.lang.IllegalArgumentException} if {@link * android.view.View#getTag()}} returns a non null object that is not an {@link * com.bumptech.glide.request.Request}. */ @Override @Nullable public Request getRequest() { Object tag = getTag(); Request request = null; if (tag != null) { if (tag instanceof Request) { request = (Request) tag; } else { throw new IllegalArgumentException( "You must not call setTag() on a view Glide is targeting"); } } return request; } @Override public String toString() { return "Target for: " + view; } private void setTag(@Nullable Object tag) { isTagUsedAtLeastOnce = true; view.setTag(tagId, tag); } @Nullable private Object getTag() { return view.getTag(tagId); } /** * Sets the android resource id to use in conjunction with {@link View#setTag(int, Object)} to * store temporary state allowing loads to be automatically cancelled and resources re-used in * scrolling lists. * *

    If no tag id is set, Glide will use {@link View#setTag(Object)}. * *

    Warning: prior to Android 4.0 tags were stored in a static map. Using this method prior to * Android 4.0 may cause memory leaks and isn't recommended. If you do use this method on older * versions, be sure to call {@link com.bumptech.glide.RequestManager#clear(View)} on any view you * start a load into to ensure that the static state is removed. * * @deprecated Glide uses it's own default tag id, so there's no need to specify your own. This * method will be removed in a future version. * @param tagId The android resource to use. */ // Public API. @SuppressWarnings("unused") @Deprecated public static void setTagId(int tagId) { if (isTagUsedAtLeastOnce) { throw new IllegalArgumentException( "You cannot set the tag id more than once or change" + " the tag id after the first request has been made"); } ViewTarget.tagId = tagId; } @VisibleForTesting static final class SizeDeterminer { // Some negative sizes (Target.SIZE_ORIGINAL) are valid, 0 is never valid. private static final int PENDING_SIZE = 0; @VisibleForTesting @Nullable static Integer maxDisplayLength; private final View view; private final List cbs = new ArrayList<>(); @Synthetic boolean waitForLayout; @Nullable private SizeDeterminerLayoutListener layoutListener; SizeDeterminer(@NonNull View view) { this.view = view; } // Use the maximum to avoid depending on the device's current orientation. private static int getMaxDisplayLength(@NonNull Context context) { if (maxDisplayLength == null) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = Preconditions.checkNotNull(windowManager).getDefaultDisplay(); Point displayDimensions = new Point(); display.getSize(displayDimensions); maxDisplayLength = Math.max(displayDimensions.x, displayDimensions.y); } return maxDisplayLength; } private void notifyCbs(int width, int height) { // One or more callbacks may trigger the removal of one or more additional callbacks, so we // need a copy of the list to avoid a concurrent modification exception. One place this // happens is when a full request completes from the in memory cache while its thumbnail is // still being loaded asynchronously. See #2237. for (SizeReadyCallback cb : new ArrayList<>(cbs)) { cb.onSizeReady(width, height); } } @Synthetic void checkCurrentDimens() { if (cbs.isEmpty()) { return; } int currentWidth = getTargetWidth(); int currentHeight = getTargetHeight(); if (!isViewStateAndSizeValid(currentWidth, currentHeight)) { return; } notifyCbs(currentWidth, currentHeight); clearCallbacksAndListener(); } void getSize(@NonNull SizeReadyCallback cb) { int currentWidth = getTargetWidth(); int currentHeight = getTargetHeight(); if (isViewStateAndSizeValid(currentWidth, currentHeight)) { cb.onSizeReady(currentWidth, currentHeight); return; } // We want to notify callbacks in the order they were added and we only expect one or two // callbacks to be added a time, so a List is a reasonable choice. if (!cbs.contains(cb)) { cbs.add(cb); } if (layoutListener == null) { ViewTreeObserver observer = view.getViewTreeObserver(); layoutListener = new SizeDeterminerLayoutListener(this); observer.addOnPreDrawListener(layoutListener); } } /** * The callback may be called anyway if it is removed by another {@link SizeReadyCallback} or * otherwise removed while we're notifying the list of callbacks. * *

    See #2237. */ void removeCallback(@NonNull SizeReadyCallback cb) { cbs.remove(cb); } void clearCallbacksAndListener() { // Keep a reference to the layout attachStateListener and remove it here // rather than having the observer remove itself because the observer // we add the attachStateListener to will be almost immediately merged into // another observer and will therefore never be alive. If we instead // keep a reference to the attachStateListener and remove it here, we get the // current view tree observer and should succeed. ViewTreeObserver observer = view.getViewTreeObserver(); if (observer.isAlive()) { observer.removeOnPreDrawListener(layoutListener); } layoutListener = null; cbs.clear(); } private boolean isViewStateAndSizeValid(int width, int height) { return isDimensionValid(width) && isDimensionValid(height); } private int getTargetHeight() { int verticalPadding = view.getPaddingTop() + view.getPaddingBottom(); LayoutParams layoutParams = view.getLayoutParams(); int layoutParamSize = layoutParams != null ? layoutParams.height : PENDING_SIZE; return getTargetDimen(view.getHeight(), layoutParamSize, verticalPadding); } private int getTargetWidth() { int horizontalPadding = view.getPaddingLeft() + view.getPaddingRight(); LayoutParams layoutParams = view.getLayoutParams(); int layoutParamSize = layoutParams != null ? layoutParams.width : PENDING_SIZE; return getTargetDimen(view.getWidth(), layoutParamSize, horizontalPadding); } private int getTargetDimen(int viewSize, int paramSize, int paddingSize) { // We consider the View state as valid if the View has non-null layout params and a non-zero // layout params width and height. This is imperfect. We're making an assumption that View // parents will obey their child's layout parameters, which isn't always the case. int adjustedParamSize = paramSize - paddingSize; if (adjustedParamSize > 0) { return adjustedParamSize; } // Since we always prefer layout parameters with fixed sizes, even if waitForLayout is true, // we might as well ignore it and just return the layout parameters above if we have them. // Otherwise we should wait for a layout pass before checking the View's dimensions. if (waitForLayout && view.isLayoutRequested()) { return PENDING_SIZE; } // We also consider the View state valid if the View has a non-zero width and height. This // means that the View has gone through at least one layout pass. It does not mean the Views // width and height are from the current layout pass. For example, if a View is re-used in // RecyclerView or ListView, this width/height may be from an old position. In some cases // the dimensions of the View at the old position may be different than the dimensions of the // View in the new position because the LayoutManager/ViewParent can arbitrarily decide to // change them. Nevertheless, in most cases this should be a reasonable choice. int adjustedViewSize = viewSize - paddingSize; if (adjustedViewSize > 0) { return adjustedViewSize; } // Finally we consider the view valid if the layout parameter size is set to wrap_content. // It's difficult for Glide to figure out what to do here. Although Target.SIZE_ORIGINAL is a // coherent choice, it's extremely dangerous because original images may be much too large to // fit in memory or so large that only a couple can fit in memory, causing OOMs. If users want // the original image, they can always use .override(Target.SIZE_ORIGINAL). Since wrap_content // may never resolve to a real size unless we load something, we aim for a square whose length // is the largest screen size. That way we're loading something and that something has some // hope of being downsampled to a size that the device can support. We also log a warning that // tries to explain what Glide is doing and why some alternatives are preferable. // Since WRAP_CONTENT is sometimes used as a default layout parameter, we always wait for // layout to complete before using this fallback parameter (ConstraintLayout among others). if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) { if (Log.isLoggable(TAG, Log.INFO)) { Log.i( TAG, "Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of this" + " device's screen dimensions. If you want to load the original image and are" + " ok with the corresponding memory cost and OOMs (depending on the input size)," + " use override(Target.SIZE_ORIGINAL). Otherwise, use LayoutParams.MATCH_PARENT," + " set layout_width and layout_height to fixed dimension, or use .override()" + " with fixed dimensions."); } return getMaxDisplayLength(view.getContext()); } // If the layout parameters are < padding, the view size is < padding, or the layout // parameters are set to match_parent or wrap_content and no layout has occurred, we should // wait for layout and repeat. return PENDING_SIZE; } private boolean isDimensionValid(int size) { return size > 0 || size == SIZE_ORIGINAL; } private static final class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener { private final WeakReference sizeDeterminerRef; SizeDeterminerLayoutListener(@NonNull SizeDeterminer sizeDeterminer) { sizeDeterminerRef = new WeakReference<>(sizeDeterminer); } @Override public boolean onPreDraw() { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "OnGlobalLayoutListener called attachStateListener=" + this); } SizeDeterminer sizeDeterminer = sizeDeterminerRef.get(); if (sizeDeterminer != null) { sizeDeterminer.checkCurrentDimens(); } return true; } } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/BitmapContainerTransitionFactory.java ================================================ package com.bumptech.glide.request.transition; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import com.bumptech.glide.load.DataSource; /** * A {@link TransitionFactory} for complex types that have a {@link android.graphics.Bitmap} inside. * The transitioning bitmap is wrapped in a {@link android.graphics.drawable.BitmapDrawable}. Most * commonly used with {@link DrawableCrossFadeFactory}. * * @param The type of the composite object that contains the {@link android.graphics.Bitmap} to * be transitioned. */ public abstract class BitmapContainerTransitionFactory implements TransitionFactory { private final TransitionFactory realFactory; // Public API. @SuppressWarnings("WeakerAccess") public BitmapContainerTransitionFactory(TransitionFactory realFactory) { this.realFactory = realFactory; } @Override public Transition build(DataSource dataSource, boolean isFirstResource) { Transition transition = realFactory.build(dataSource, isFirstResource); return new BitmapGlideAnimation(transition); } /** * Retrieve the Bitmap from a composite object. * *

    Warning: Do not convert any arbitrary object to Bitmap via expensive drawing here, * this method is called on the UI thread. * * @param current composite object containing a Bitmap and some other information * @return the Bitmap contained within {@code current} */ protected abstract Bitmap getBitmap(R current); private final class BitmapGlideAnimation implements Transition { private final Transition transition; BitmapGlideAnimation(Transition transition) { this.transition = transition; } @Override public boolean transition(R current, ViewAdapter adapter) { Resources resources = adapter.getView().getResources(); Drawable currentBitmap = new BitmapDrawable(resources, getBitmap(current)); return transition.transition(currentBitmap, adapter); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/BitmapTransitionFactory.java ================================================ package com.bumptech.glide.request.transition; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; /** * A {@link TransitionFactory} for {@link android.graphics.Bitmap}s that uses a Drawable transition * factory to transition from an existing drawable already visible on the target to the new bitmap. * * @see BitmapContainerTransitionFactory */ public class BitmapTransitionFactory extends BitmapContainerTransitionFactory { public BitmapTransitionFactory(@NonNull TransitionFactory realFactory) { super(realFactory); } @Override @NonNull protected Bitmap getBitmap(@NonNull Bitmap current) { return current; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/DrawableCrossFadeFactory.java ================================================ package com.bumptech.glide.request.transition; import android.graphics.drawable.Drawable; import com.bumptech.glide.load.DataSource; /** * A factory class that produces a new {@link Transition} that varies depending on whether or not * the drawable was loaded from the memory cache and whether or not the drawable is the first image * to be put on the target. * *

    Resources are usually loaded from the memory cache just before the user can see the view, for * example when the user changes screens or scrolls back and forth in a list. In those cases the * user typically does not expect to see a transition. As a result, when the resource is loaded from * the memory cache this factory produces an {@link NoTransition}. */ // Public API. @SuppressWarnings("WeakerAccess") public class DrawableCrossFadeFactory implements TransitionFactory { private final int duration; private final boolean isCrossFadeEnabled; private DrawableCrossFadeTransition resourceTransition; protected DrawableCrossFadeFactory(int duration, boolean isCrossFadeEnabled) { this.duration = duration; this.isCrossFadeEnabled = isCrossFadeEnabled; } @Override public Transition build(DataSource dataSource, boolean isFirstResource) { return dataSource == DataSource.MEMORY_CACHE ? NoTransition.get() : getResourceTransition(); } private Transition getResourceTransition() { if (resourceTransition == null) { resourceTransition = new DrawableCrossFadeTransition(duration, isCrossFadeEnabled); } return resourceTransition; } /** A Builder for {@link DrawableCrossFadeFactory}. */ @SuppressWarnings("unused") public static class Builder { private static final int DEFAULT_DURATION_MS = 300; private final int durationMillis; private boolean isCrossFadeEnabled; public Builder() { this(DEFAULT_DURATION_MS); } /** * @param durationMillis The duration of the cross fade animation in milliseconds. */ public Builder(int durationMillis) { this.durationMillis = durationMillis; } /** * Enables or disables animating the alpha of the {@link Drawable} the cross fade will animate * from. * *

    Defaults to {@code false}. * * @param isCrossFadeEnabled If {@code true} the previous {@link Drawable}'s alpha will be * animated from 100 to 0 while the new {@link Drawable}'s alpha is animated from 0 to 100. * Otherwise the previous {@link Drawable}'s alpha will remain at 100 throughout the * animation. See {@link * android.graphics.drawable.TransitionDrawable#setCrossFadeEnabled(boolean)} */ public Builder setCrossFadeEnabled(boolean isCrossFadeEnabled) { this.isCrossFadeEnabled = isCrossFadeEnabled; return this; } public DrawableCrossFadeFactory build() { return new DrawableCrossFadeFactory(durationMillis, isCrossFadeEnabled); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/DrawableCrossFadeTransition.java ================================================ package com.bumptech.glide.request.transition; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; /** * A cross fade {@link Transition} for {@link android.graphics.drawable.Drawable}s that uses an * {@link android.graphics.drawable.TransitionDrawable} to transition from an existing drawable * already visible on the target to a new drawable. If no existing drawable exists, this class can * instead fall back to a default animation that doesn't rely on {@link * android.graphics.drawable.TransitionDrawable}. */ public class DrawableCrossFadeTransition implements Transition { private final int duration; private final boolean isCrossFadeEnabled; /** * @param duration The duration that the cross fade animation should run if there is something to * cross fade from when a new {@link android.graphics.drawable.Drawable} is put. * @param isCrossFadeEnabled If {@code true}, animates the previous resource's alpha to 0 while * animating the new resource's alpha to 100. Otherwise, only animates the new resource's * alpha to 100 while leaving the previous resource's alpha at 100. See {@link * TransitionDrawable#setCrossFadeEnabled(boolean)}. */ // Public API. @SuppressWarnings("WeakerAccess") public DrawableCrossFadeTransition(int duration, boolean isCrossFadeEnabled) { this.duration = duration; this.isCrossFadeEnabled = isCrossFadeEnabled; } /** * Animates from the previous drawable to the current drawable in one of two ways. * *

      *
    1. Using the default animation provided in the constructor if the previous drawable is null *
    2. Using the cross fade animation with the duration provided in the constructor if the * previous drawable is non null *
    * * @param current {@inheritDoc} * @param adapter {@inheritDoc} * @return {@inheritDoc} */ @Override public boolean transition(Drawable current, ViewAdapter adapter) { Drawable previous = adapter.getCurrentDrawable(); if (previous == null) { previous = new ColorDrawable(Color.TRANSPARENT); } TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] {previous, current}); transitionDrawable.setCrossFadeEnabled(isCrossFadeEnabled); transitionDrawable.startTransition(duration); adapter.setDrawable(transitionDrawable); return true; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/NoTransition.java ================================================ package com.bumptech.glide.request.transition; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.util.Synthetic; /** * A simple {@link Transition} that performs no actions. * * @param the resource type that will be transitioned into a {@link * com.bumptech.glide.request.target.Target}. */ public class NoTransition implements Transition { @Synthetic static final NoTransition NO_ANIMATION = new NoTransition<>(); @SuppressWarnings("rawtypes") private static final TransitionFactory NO_ANIMATION_FACTORY = new NoAnimationFactory(); /** * A factory that always returns the same {@link NoTransition}. * * @param the resource type that will be transitioned into a {@link * com.bumptech.glide.request.target.Target}. */ public static class NoAnimationFactory implements TransitionFactory { @SuppressWarnings("unchecked") @Override public Transition build(DataSource dataSource, boolean isFirstResource) { return (Transition) NO_ANIMATION; } } /** Returns an instance of a factory that produces {@link NoTransition}s. */ @SuppressWarnings("unchecked") public static TransitionFactory getFactory() { return (TransitionFactory) NO_ANIMATION_FACTORY; } /** Returns an instance of {@link NoTransition}. */ @SuppressWarnings("unchecked") public static Transition get() { return (Transition) NO_ANIMATION; } /** Performs no animation and always returns {@code false}. */ @Override public boolean transition(Object current, ViewAdapter adapter) { return false; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/Transition.java ================================================ package com.bumptech.glide.request.transition; import android.graphics.drawable.Drawable; import android.view.View; import androidx.annotation.Nullable; /** * An interface that allows a transition to be applied to {@link android.view.View}s in {@link * com.bumptech.glide.request.target.Target}s in across resource types. Targets that wrap views will * be able to provide all of the necessary arguments and start the transition. Those that do not * will be unable to provide the necessary arguments and will therefore be forced to ignore the * transition. This interface is a compromise that allows view specific transition in Glide's * complex world of arbitrary resource types and arbitrary target types. * * @param The type of the resource whose entrance will be transitioned. */ public interface Transition { /** * An interface wrapping a view that exposes the necessary methods to run the various types of * android animations as transitions: ({@link ViewTransition}, {@link ViewPropertyTransition} and * animated {@link android.graphics.drawable.Drawable}s). */ interface ViewAdapter { /** Returns the wrapped {@link android.view.View}. */ View getView(); /** * Returns the current drawable being displayed in the view, or null if no such drawable exists * (or one cannot be retrieved). */ @Nullable Drawable getCurrentDrawable(); /** * Sets the current drawable (usually an animated drawable) to display in the wrapped view. * * @param drawable The drawable to display in the wrapped view. */ void setDrawable(Drawable drawable); } /** * Animates from the previous {@link android.graphics.drawable.Drawable} that is currently being * displayed in the given view, if not null, to the new resource that should be displayed in the * view. * * @param current The new resource that will be displayed in the view. * @param adapter The {@link Transition.ViewAdapter} wrapping a view that can at least return an * {@link android.view.View} from {@link Transition.ViewAdapter#getView()}. * @return True if in the process of running the transition, the new resource was put on the view, * false if the caller needs to manually put the current resource on the view. */ boolean transition(R current, ViewAdapter adapter); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/TransitionFactory.java ================================================ package com.bumptech.glide.request.transition; import com.bumptech.glide.load.DataSource; /** * A factory class that can produce different {@link Transition}s based on the state of the request. * * @param The type of resource that needs to be animated into the target. */ public interface TransitionFactory { /** * Returns a new {@link Transition}. * * @param dataSource The {@link com.bumptech.glide.load.DataSource} the resource was loaded from. * @param isFirstResource True if this is the first resource to be loaded into the target. */ Transition build(DataSource dataSource, boolean isFirstResource); } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/ViewAnimationFactory.java ================================================ package com.bumptech.glide.request.transition; import android.content.Context; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import com.bumptech.glide.load.DataSource; /** * A {@link TransitionFactory} that produces {@link ViewTransition}s. * * @param The type of the resource that will be transitioned into a view. */ public class ViewAnimationFactory implements TransitionFactory { private final ViewTransition.ViewTransitionAnimationFactory viewTransitionAnimationFactory; private Transition transition; // Public API. @SuppressWarnings("unused") public ViewAnimationFactory(Animation animation) { this(new ConcreteViewTransitionAnimationFactory(animation)); } public ViewAnimationFactory(int animationId) { this(new ResourceViewTransitionAnimationFactory(animationId)); } ViewAnimationFactory( ViewTransition.ViewTransitionAnimationFactory viewTransitionAnimationFactory) { this.viewTransitionAnimationFactory = viewTransitionAnimationFactory; } /** * Returns a new {@link Transition} for the given arguments. If isFromMemoryCache is {@code true} * or isFirstImage is {@code false}, returns a {@link NoTransition} and otherwise returns a new * {@link ViewTransition}. * * @param dataSource {@inheritDoc} * @param isFirstResource {@inheritDoc} */ @Override public Transition build(DataSource dataSource, boolean isFirstResource) { if (dataSource == DataSource.MEMORY_CACHE || !isFirstResource) { return NoTransition.get(); } if (transition == null) { transition = new ViewTransition<>(viewTransitionAnimationFactory); } return transition; } private static class ConcreteViewTransitionAnimationFactory implements ViewTransition.ViewTransitionAnimationFactory { private final Animation animation; ConcreteViewTransitionAnimationFactory(Animation animation) { this.animation = animation; } @Override public Animation build(Context context) { return animation; } } private static class ResourceViewTransitionAnimationFactory implements ViewTransition.ViewTransitionAnimationFactory { private final int animationId; ResourceViewTransitionAnimationFactory(int animationId) { this.animationId = animationId; } @Override public Animation build(Context context) { return AnimationUtils.loadAnimation(context, animationId); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/ViewPropertyAnimationFactory.java ================================================ package com.bumptech.glide.request.transition; import com.bumptech.glide.load.DataSource; /** * A {@link TransitionFactory} that produces ViewPropertyAnimations. * * @param The type of the resource that will be transitioned into a view. */ public class ViewPropertyAnimationFactory implements TransitionFactory { private final ViewPropertyTransition.Animator animator; private ViewPropertyTransition animation; public ViewPropertyAnimationFactory(ViewPropertyTransition.Animator animator) { this.animator = animator; } /** * Returns a new {@link Transition} for the given arguments. If isMemoryCache is {@code true} or * isFirstImage is {@code false}, returns a {@link NoTransition} and otherwise returns a new * {@link ViewPropertyTransition} for the {@link ViewPropertyTransition.Animator} provided in the * constructor. */ @Override public Transition build(DataSource dataSource, boolean isFirstResource) { if (dataSource == DataSource.MEMORY_CACHE || !isFirstResource) { return NoTransition.get(); } if (animation == null) { animation = new ViewPropertyTransition<>(animator); } return animation; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/ViewPropertyTransition.java ================================================ package com.bumptech.glide.request.transition; import android.view.View; /** * A {@link Transition} that accepts an interface that can apply an animation like a {@link * android.view.ViewPropertyAnimator} or a {@link android.animation.ObjectAnimator} that can be used * to transition a resource into a {@link View}. * * @param The type of the resource that will be transitioned into a view. */ public class ViewPropertyTransition implements Transition { private final Animator animator; /** * Constructor for a view property animation that takes an {@link ViewPropertyTransition.Animator} * interface that can apply a transition to a view. * * @param animator The animator to use. */ // Public API. @SuppressWarnings("WeakerAccess") public ViewPropertyTransition(Animator animator) { this.animator = animator; } /** * Always applies the {@link ViewPropertyTransition.Animator} given in the constructor to the * given view and returns {@code false} because the animator cannot put the new resource on the * view. * * @param current {@inheritDoc} * @param adapter {@inheritDoc} * @return {@inheritDoc} */ @Override public boolean transition(R current, ViewAdapter adapter) { final View view = adapter.getView(); if (view != null) { animator.animate(adapter.getView()); } return false; } /** * An interface that allows an animation to be applied on or started from an {@link * android.view.View}. */ public interface Animator { /** * Starts an animation on the given {@link android.view.View}. * * @param view The view to transition. */ void animate(View view); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/request/transition/ViewTransition.java ================================================ package com.bumptech.glide.request.transition; import android.content.Context; import android.view.View; import android.view.animation.Animation; /** * A {@link Transition} that can apply a {@link android.view.animation.Animation Animation} to a * {@link android.view.View View} using {@link * android.view.View#startAnimation(android.view.animation.Animation)}. * * @param The type of the resource that will be transitioned into a view. */ public class ViewTransition implements Transition { private final ViewTransitionAnimationFactory viewTransitionAnimationFactory; /** * Constructs a new ViewAnimation that will start the given {@link android.view.animation * .Animation}. */ ViewTransition(ViewTransitionAnimationFactory viewTransitionAnimationFactory) { this.viewTransitionAnimationFactory = viewTransitionAnimationFactory; } /** * Always clears the current animation on the view using {@link * android.view.View#clearAnimation()}, then starts the {@link android.view.animation.Animation} * given in the constructor using {@link * android.view.View#startAnimation(android.view.animation.Animation)} and then returns {@code * false} because the animation does not actually put the current resource on the view. * * @param current {@inheritDoc} * @param adapter {@inheritDoc} * @return {@inheritDoc} */ @Override public boolean transition(R current, ViewAdapter adapter) { View view = adapter.getView(); if (view != null) { view.clearAnimation(); Animation animation = viewTransitionAnimationFactory.build(view.getContext()); view.startAnimation(animation); } return false; } interface ViewTransitionAnimationFactory { Animation build(Context context); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/signature/AndroidResourceSignature.java ================================================ package com.bumptech.glide.signature; import android.content.Context; import android.content.res.Configuration; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.util.Util; import java.nio.ByteBuffer; import java.security.MessageDigest; /** Includes information about the package as well as whether or not the device is in night mode. */ public final class AndroidResourceSignature implements Key { private final int nightMode; private final Key applicationVersion; @NonNull public static Key obtain(@NonNull Context context) { Key signature = ApplicationVersionSignature.obtain(context); int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; return new AndroidResourceSignature(nightMode, signature); } private AndroidResourceSignature(int nightMode, Key applicationVersion) { this.nightMode = nightMode; this.applicationVersion = applicationVersion; } @Override public boolean equals(Object o) { if (o instanceof AndroidResourceSignature) { AndroidResourceSignature that = (AndroidResourceSignature) o; return nightMode == that.nightMode && applicationVersion.equals(that.applicationVersion); } return false; } @Override public int hashCode() { return Util.hashCode(applicationVersion, nightMode); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { applicationVersion.updateDiskCacheKey(messageDigest); byte[] nightModeData = ByteBuffer.allocate(4).putInt(nightMode).array(); messageDigest.update(nightModeData); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/signature/ApplicationVersionSignature.java ================================================ package com.bumptech.glide.signature; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.bumptech.glide.load.Key; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * A utility class for obtaining a {@link com.bumptech.glide.load.Key} signature containing the * application version name using {@link android.content.pm.PackageInfo#versionCode}. */ public final class ApplicationVersionSignature { private static final String TAG = "AppVersionSignature"; private static final ConcurrentMap PACKAGE_NAME_TO_KEY = new ConcurrentHashMap<>(); /** * Returns the signature {@link com.bumptech.glide.load.Key} for version code of the Application * of the given Context. */ @NonNull public static Key obtain(@NonNull Context context) { String packageName = context.getPackageName(); Key result = PACKAGE_NAME_TO_KEY.get(packageName); if (result == null) { Key toAdd = obtainVersionSignature(context); result = PACKAGE_NAME_TO_KEY.putIfAbsent(packageName, toAdd); // There wasn't a previous mapping, so toAdd is now the Key. if (result == null) { result = toAdd; } } return result; } @VisibleForTesting static void reset() { PACKAGE_NAME_TO_KEY.clear(); } @NonNull private static Key obtainVersionSignature(@NonNull Context context) { PackageInfo packageInfo = getPackageInfo(context); String versionCode = getVersionCode(packageInfo); return new ObjectKey(versionCode); } @NonNull private static String getVersionCode(@Nullable PackageInfo packageInfo) { String versionCode; if (packageInfo != null) { versionCode = String.valueOf(packageInfo.versionCode); } else { versionCode = UUID.randomUUID().toString(); } return versionCode; } @Nullable private static PackageInfo getPackageInfo(@NonNull Context context) { try { return context.getPackageManager().getPackageInfo(context.getPackageName(), 0); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Cannot resolve info for" + context.getPackageName(), e); return null; } } private ApplicationVersionSignature() { // Empty for visibility. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/signature/EmptySignature.java ================================================ package com.bumptech.glide.signature; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import java.security.MessageDigest; /** An empty key that is always equal to all other empty keys. */ public final class EmptySignature implements Key { private static final EmptySignature EMPTY_KEY = new EmptySignature(); @NonNull public static EmptySignature obtain() { return EMPTY_KEY; } private EmptySignature() { // Empty. } @Override public String toString() { return "EmptySignature"; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { // Do nothing. } } ================================================ FILE: library/src/main/java/com/bumptech/glide/signature/MediaStoreSignature.java ================================================ package com.bumptech.glide.signature; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Key; import java.nio.ByteBuffer; import java.security.MessageDigest; /** * A unique signature based on metadata data from the media store that detects common changes to * media store files like edits, rotations, and temporary file replacement. */ public class MediaStoreSignature implements Key { @NonNull private final String mimeType; private final long dateModified; private final int orientation; /** * Constructor for {@link com.bumptech.glide.signature.MediaStoreSignature}. * * @param mimeType The mime type of the media store media. Ok to default to empty string "". See * {@link android.provider.MediaStore.Images.ImageColumns#MIME_TYPE} or {@link * android.provider.MediaStore.Video.VideoColumns#MIME_TYPE}. * @param dateModified The date modified time of the media store media. Ok to default to 0. See * {@link android.provider.MediaStore.Images.ImageColumns#DATE_MODIFIED} or {@link * android.provider.MediaStore.Video.VideoColumns#DATE_MODIFIED}. * @param orientation The orientation of the media store media. Ok to default to 0. See {@link * android.provider.MediaStore.Images.ImageColumns#ORIENTATION}. */ public MediaStoreSignature(@Nullable String mimeType, long dateModified, int orientation) { this.mimeType = mimeType == null ? "" : mimeType; this.dateModified = dateModified; this.orientation = orientation; } @SuppressWarnings({"PMD.SimplifyBooleanReturns", "RedundantIfStatement"}) @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MediaStoreSignature that = (MediaStoreSignature) o; if (dateModified != that.dateModified) { return false; } if (orientation != that.orientation) { return false; } if (!mimeType.equals(that.mimeType)) { return false; } return true; } @Override public int hashCode() { int result = mimeType.hashCode(); result = 31 * result + (int) (dateModified ^ (dateModified >>> 32)); result = 31 * result + orientation; return result; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { byte[] data = ByteBuffer.allocate(12).putLong(dateModified).putInt(orientation).array(); messageDigest.update(data); messageDigest.update(mimeType.getBytes(CHARSET)); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/signature/ObjectKey.java ================================================ package com.bumptech.glide.signature; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.util.Preconditions; import java.security.MessageDigest; /** * Wraps an {@link java.lang.Object}, delegating {@link #equals(Object)} and {@link #hashCode()} to * the wrapped Object and providing the bytes of the result of the Object's {@link #toString()} * method to the {@link java.security.MessageDigest} in {@link * #updateDiskCacheKey(java.security.MessageDigest)}. * *

    The Object's {@link #toString()} method must be unique and suitable for use as a disk cache * key. */ public final class ObjectKey implements Key { private final Object object; public ObjectKey(@NonNull Object object) { this.object = Preconditions.checkNotNull(object); } @Override public String toString() { return "ObjectKey{" + "object=" + object + '}'; } @Override public boolean equals(Object o) { if (o instanceof ObjectKey) { ObjectKey other = (ObjectKey) o; return object.equals(other.object); } return false; } @Override public int hashCode() { return object.hashCode(); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(object.toString().getBytes(CHARSET)); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/ByteBufferUtil.java ================================================ package com.bumptech.glide.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.concurrent.atomic.AtomicReference; /** Utilities for interacting with {@link java.nio.ByteBuffer}s. */ @SuppressWarnings({"unused", "WeakerAccess"}) // Public API public final class ByteBufferUtil { // 16 Kb private static final int BUFFER_SIZE = 16384; private static final AtomicReference BUFFER_REF = new AtomicReference<>(); private ByteBufferUtil() { // Utility class. } @NonNull public static ByteBuffer fromFile(@NonNull File file) throws IOException { RandomAccessFile raf = null; FileChannel channel = null; try { long fileLength = file.length(); // See #2240. if (fileLength > Integer.MAX_VALUE) { throw new IOException("File too large to map into memory"); } // See b/67710449. if (fileLength == 0) { throw new IOException("File unsuitable for memory mapping"); } raf = new RandomAccessFile(file, "r"); channel = raf.getChannel(); return channel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength).load(); } finally { if (channel != null) { try { channel.close(); } catch (IOException e) { // Ignored. } } if (raf != null) { try { raf.close(); } catch (IOException e) { // Ignored. } } } } public static void toFile(@NonNull ByteBuffer buffer, @NonNull File file) throws IOException { rewind(buffer); RandomAccessFile raf = null; FileChannel channel = null; try { raf = new RandomAccessFile(file, "rw"); channel = raf.getChannel(); channel.write(buffer); channel.force(false /*metadata*/); channel.close(); raf.close(); } finally { if (channel != null) { try { channel.close(); } catch (IOException e) { // Ignored. } } if (raf != null) { try { raf.close(); } catch (IOException e) { // Ignored. } } } } public static void toStream(@NonNull ByteBuffer byteBuffer, @NonNull OutputStream os) throws IOException { SafeArray safeArray = getSafeArray(byteBuffer); if (safeArray != null) { os.write(safeArray.data, safeArray.offset, safeArray.offset + safeArray.limit); } else { byte[] buffer = BUFFER_REF.getAndSet(null); if (buffer == null) { buffer = new byte[BUFFER_SIZE]; } while (byteBuffer.remaining() > 0) { int toRead = Math.min(byteBuffer.remaining(), buffer.length); byteBuffer.get(buffer, 0 /*dstOffset*/, toRead /*byteCount*/); os.write(buffer, 0, toRead); } BUFFER_REF.set(buffer); } } // We check the appropriate offsets, so this is a spurious warning. @SuppressWarnings("ByteBufferBackingArray") @NonNull public static byte[] toBytes(@NonNull ByteBuffer byteBuffer) { final byte[] result; SafeArray safeArray = getSafeArray(byteBuffer); if (safeArray != null && safeArray.offset == 0 && safeArray.limit == safeArray.data.length) { result = byteBuffer.array(); } else { ByteBuffer toCopy = byteBuffer.asReadOnlyBuffer(); result = new byte[toCopy.limit()]; rewind(toCopy); toCopy.get(result); } return result; } @NonNull public static InputStream toStream(@NonNull ByteBuffer buffer) { return new ByteBufferStream(buffer); } @NonNull public static ByteBuffer fromStream(@NonNull InputStream stream) throws IOException { ByteArrayOutputStream outStream = new ByteArrayOutputStream(BUFFER_SIZE); byte[] buffer = BUFFER_REF.getAndSet(null); if (buffer == null) { buffer = new byte[BUFFER_SIZE]; } int n; while ((n = stream.read(buffer)) >= 0) { outStream.write(buffer, 0, n); } BUFFER_REF.set(buffer); byte[] bytes = outStream.toByteArray(); // Some resource decoders require a direct byte buffer. Prefer allocateDirect() over wrap() return rewind(ByteBuffer.allocateDirect(bytes.length).put(bytes)); } public static ByteBuffer rewind(ByteBuffer buffer) { return (ByteBuffer) buffer.position(0); } @Nullable private static SafeArray getSafeArray(@NonNull ByteBuffer byteBuffer) { if (!byteBuffer.isReadOnly() && byteBuffer.hasArray()) { return new SafeArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit()); } return null; } static final class SafeArray { @Synthetic final int offset; @Synthetic final int limit; @Synthetic final byte[] data; // PMD.ArrayIsStoredDirectly Copying would be prohibitively expensive and/or lead to OOMs. @SuppressWarnings("PMD.ArrayIsStoredDirectly") SafeArray(@NonNull byte[] data, int offset, int limit) { this.data = data; this.offset = offset; this.limit = limit; } } private static class ByteBufferStream extends InputStream { private static final int UNSET = -1; @NonNull private final ByteBuffer byteBuffer; private int markPos = UNSET; ByteBufferStream(@NonNull ByteBuffer byteBuffer) { this.byteBuffer = byteBuffer; } @Override public int available() { return byteBuffer.remaining(); } @Override public int read() { if (!byteBuffer.hasRemaining()) { return -1; } return byteBuffer.get() & 0xFF; } @Override public synchronized void mark(int readLimit) { markPos = byteBuffer.position(); } @Override public boolean markSupported() { return true; } @Override public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) { if (!byteBuffer.hasRemaining()) { return -1; } int toRead = Math.min(byteCount, available()); byteBuffer.get(buffer, byteOffset, toRead); return toRead; } @Override public synchronized void reset() throws IOException { if (markPos == UNSET) { throw new IOException("Cannot reset to unset mark position"); } // reset() was not implemented correctly in 4.0.4, so we track the mark position ourselves. byteBuffer.position(markPos); } @Override public long skip(long byteCount) { if (!byteBuffer.hasRemaining()) { return -1; } long toSkip = Math.min(byteCount, available()); byteBuffer.position((int) (byteBuffer.position() + toSkip)); return toSkip; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/CachedHashCodeArrayMap.java ================================================ package com.bumptech.glide.util; import androidx.collection.ArrayMap; import androidx.collection.SimpleArrayMap; /** * An {@link ArrayMap} that caches its hashCode to support efficient lookup. * * @param the key type. * @param the value type. */ // We're overriding hashcode, but not in a way that changes the output, so we don't need to // override equals. @SuppressWarnings("PMD.OverrideBothEqualsAndHashcode") public final class CachedHashCodeArrayMap extends ArrayMap { private int hashCode; @Override public void clear() { hashCode = 0; super.clear(); } @Override public V setValueAt(int index, V value) { hashCode = 0; return super.setValueAt(index, value); } @Override public V put(K key, V value) { hashCode = 0; return super.put(key, value); } @Override public void putAll(SimpleArrayMap simpleArrayMap) { hashCode = 0; super.putAll(simpleArrayMap); } @Override public V removeAt(int index) { hashCode = 0; return super.removeAt(index); } @Override public int hashCode() { if (hashCode == 0) { hashCode = super.hashCode(); } return hashCode; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/ContentLengthInputStream.java ================================================ package com.bumptech.glide.util; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * Uses the content length as the basis for the return value of {@link #available()} and verifies * that at least content length bytes are returned from the various read methods. */ public final class ContentLengthInputStream extends FilterInputStream { private static final String TAG = "ContentLengthStream"; private static final int UNKNOWN = -1; private final long contentLength; private int readSoFar; @NonNull public static InputStream obtain( @NonNull InputStream other, @Nullable String contentLengthHeader) { return obtain(other, parseContentLength(contentLengthHeader)); } @NonNull public static InputStream obtain(@NonNull InputStream other, long contentLength) { return new ContentLengthInputStream(other, contentLength); } private static int parseContentLength(@Nullable String contentLengthHeader) { int result = UNKNOWN; if (!TextUtils.isEmpty(contentLengthHeader)) { try { result = Integer.parseInt(contentLengthHeader); } catch (NumberFormatException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "failed to parse content length header: " + contentLengthHeader, e); } } } return result; } private ContentLengthInputStream(@NonNull InputStream in, long contentLength) { super(in); this.contentLength = contentLength; } @Override public synchronized int available() throws IOException { return (int) Math.max(contentLength - readSoFar, in.available()); } @Override public synchronized int read() throws IOException { int value = super.read(); checkReadSoFarOrThrow(value >= 0 ? 1 : -1); return value; } @Override public int read(byte[] buffer) throws IOException { return read(buffer, 0 /*byteOffset*/, buffer.length /*byteCount*/); } @Override public synchronized int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { return checkReadSoFarOrThrow(super.read(buffer, byteOffset, byteCount)); } private int checkReadSoFarOrThrow(int read) throws IOException { if (read >= 0) { readSoFar += read; } else if (contentLength - readSoFar > 0) { throw new IOException( "Failed to read all expected data" + ", expected: " + contentLength + ", but read: " + readSoFar); } return read; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/ExceptionCatchingInputStream.java ================================================ package com.bumptech.glide.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.util.Queue; /** * An {@link java.io.InputStream} that catches {@link java.io.IOException}s during read and skip * calls and stores them so they can later be handled or thrown. This class is a workaround for a * framework issue where exceptions during reads while decoding bitmaps in {@link * android.graphics.BitmapFactory} can return partially decoded bitmaps. * *

    See https://github.com/bumptech/glide/issues/126. * * @deprecated In some cases, callers may not handle getting 0 or -1 return values from methods, * which can lead to infinite loops (see #4438). Use {@link ExceptionPassthroughInputStream} * instead. This class will be deleted in a future version of Glide. */ @Deprecated public class ExceptionCatchingInputStream extends InputStream { private static final Queue QUEUE = Util.createQueue(0); private InputStream wrapped; private IOException exception; @NonNull public static ExceptionCatchingInputStream obtain(@NonNull InputStream toWrap) { ExceptionCatchingInputStream result; synchronized (QUEUE) { result = QUEUE.poll(); } if (result == null) { result = new ExceptionCatchingInputStream(); } result.setInputStream(toWrap); return result; } // Exposed for testing. static void clearQueue() { while (!QUEUE.isEmpty()) { QUEUE.remove(); } } ExceptionCatchingInputStream() { // Do nothing. } void setInputStream(@NonNull InputStream toWrap) { wrapped = toWrap; } @Override public int available() throws IOException { return wrapped.available(); } @Override public void close() throws IOException { wrapped.close(); } @Override public void mark(int readLimit) { wrapped.mark(readLimit); } @Override public boolean markSupported() { return wrapped.markSupported(); } @Override public int read(byte[] buffer) { int read; try { read = wrapped.read(buffer); } catch (IOException e) { exception = e; read = -1; } return read; } @Override public int read(byte[] buffer, int byteOffset, int byteCount) { int read; try { read = wrapped.read(buffer, byteOffset, byteCount); } catch (IOException e) { exception = e; read = -1; } return read; } @Override public synchronized void reset() throws IOException { wrapped.reset(); } @Override public long skip(long byteCount) { long skipped; try { skipped = wrapped.skip(byteCount); } catch (IOException e) { exception = e; skipped = 0; } return skipped; } @Override public int read() { int result; try { result = wrapped.read(); } catch (IOException e) { exception = e; result = -1; } return result; } @Nullable public IOException getException() { return exception; } public void release() { exception = null; wrapped = null; synchronized (QUEUE) { QUEUE.offer(this); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/ExceptionPassthroughInputStream.java ================================================ package com.bumptech.glide.util; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.util.Queue; /** * An {@link java.io.InputStream} that catches, stores and rethrows {@link java.io.IOException}s * during read and skip calls. This allows users of this API to handle the exception at a higher * level if the exception is swallowed by some intermediate library. This class is a workaround for * a framework issue where exceptions during reads while decoding bitmaps in {@link * android.graphics.BitmapFactory} can return partially decoded bitmaps. * *

    Unlike the deprecated {@link ExceptionCatchingInputStream}, this class will both store and * re-throw any IOExceptions. Rethrowing works around bugs in wrapping streams that may not fully * obey the stream contract. This is really only useful if some middle layer is going to catch the * exception (like BitmapFactory) but we want to propagate the exception instead. * *

    See https://github.com/bumptech/glide/issues/126 and #4438. */ public final class ExceptionPassthroughInputStream extends InputStream { @GuardedBy("POOL") private static final Queue POOL = Util.createQueue(0); private InputStream wrapped; private IOException exception; @NonNull public static ExceptionPassthroughInputStream obtain(@NonNull InputStream toWrap) { ExceptionPassthroughInputStream result; synchronized (POOL) { result = POOL.poll(); } if (result == null) { result = new ExceptionPassthroughInputStream(); } result.setInputStream(toWrap); return result; } // Exposed for testing. static void clearQueue() { synchronized (POOL) { while (!POOL.isEmpty()) { POOL.remove(); } } } ExceptionPassthroughInputStream() { // Do nothing. } void setInputStream(@NonNull InputStream toWrap) { wrapped = toWrap; } @Override public int available() throws IOException { return wrapped.available(); } @Override public void close() throws IOException { wrapped.close(); } @Override public void mark(int readLimit) { wrapped.mark(readLimit); } @Override public boolean markSupported() { return wrapped.markSupported(); } @Override public int read() throws IOException { try { return wrapped.read(); } catch (IOException e) { exception = e; throw e; } } @Override public int read(byte[] buffer) throws IOException { try { return wrapped.read(buffer); } catch (IOException e) { exception = e; throw e; } } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { try { return wrapped.read(buffer, byteOffset, byteCount); } catch (IOException e) { exception = e; throw e; } } @Override public synchronized void reset() throws IOException { wrapped.reset(); } @Override public long skip(long byteCount) throws IOException { try { return wrapped.skip(byteCount); } catch (IOException e) { exception = e; throw e; } } @Nullable public IOException getException() { return exception; } public void release() { exception = null; wrapped = null; synchronized (POOL) { POOL.offer(this); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/Executors.java ================================================ package com.bumptech.glide.util; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; /** Generic {@link Executor} implementations. */ public final class Executors { private Executors() { // Utility class. } private static final Executor MAIN_THREAD_EXECUTOR = new Executor() { @Override public void execute(@NonNull Runnable command) { Util.postOnUiThread(command); } }; private static final Executor MAIN_THREAD_EXECUTOR_FRONT = new Executor() { @Override public void execute(@NonNull Runnable command) { Util.postAtFrontOfQueueOnUiThread(command); } }; private static final Executor DIRECT_EXECUTOR = new Executor() { @Override public void execute(@NonNull Runnable command) { command.run(); } }; /** Posts executions to the main thread. */ public static Executor mainThreadExecutor() { return MAIN_THREAD_EXECUTOR; } /** Posts executions to the main thread at the front of the queue. */ public static Executor mainThreadExecutorFront() { return MAIN_THREAD_EXECUTOR_FRONT; } /** Immediately calls {@link Runnable#run()} on the current thread. */ public static Executor directExecutor() { return DIRECT_EXECUTOR; } @VisibleForTesting public static void shutdownAndAwaitTermination(ExecutorService pool) { long shutdownSeconds = 5; pool.shutdownNow(); try { if (!pool.awaitTermination(shutdownSeconds, TimeUnit.SECONDS)) { pool.shutdownNow(); if (!pool.awaitTermination(shutdownSeconds, TimeUnit.SECONDS)) { throw new RuntimeException("Failed to shutdown"); } } } catch (InterruptedException ie) { pool.shutdownNow(); Thread.currentThread().interrupt(); throw new RuntimeException(ie); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/FixedPreloadSizeProvider.java ================================================ package com.bumptech.glide.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.ListPreloader; /** * A {@link com.bumptech.glide.ListPreloader.PreloadSizeProvider} with a fixed width and height. * * @param The type of the model the size should be provided for. */ public class FixedPreloadSizeProvider implements ListPreloader.PreloadSizeProvider { private final int[] size; /** * Constructor for a PreloadSizeProvider with a fixed size. * * @param width The width of the preload size in pixels. * @param height The height of the preload size in pixels. */ public FixedPreloadSizeProvider(int width, int height) { this.size = new int[] {width, height}; } @Nullable @Override // It's better to take on the risk that callers may mutate the array when there isn't any reason // for them to do so than it the performance overhead of copying the array with every call. @SuppressWarnings("PMD.MethodReturnsInternalArray") public int[] getPreloadSize(@NonNull T item, int adapterPosition, int itemPosition) { return size; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/GlideSuppliers.java ================================================ package com.bumptech.glide.util; /** Similar to {@link com.google.common.base.Suppliers}, but named to reduce import confusion. */ public final class GlideSuppliers { /** * Produces a non-null instance of {@code T}. * * @param The data type */ public interface GlideSupplier { T get(); } private GlideSuppliers() {} public static GlideSupplier memorize(final GlideSupplier supplier) { return new GlideSupplier() { private volatile T instance; @Override public T get() { if (instance == null) { synchronized (this) { if (instance == null) { instance = Preconditions.checkNotNull(supplier.get()); } } } return instance; } }; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/LogTime.java ================================================ package com.bumptech.glide.util; import android.annotation.TargetApi; import android.os.Build; import android.os.SystemClock; /** A class for logging elapsed real time in millis. */ public final class LogTime { private static final double MILLIS_MULTIPLIER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ? 1d / Math.pow(10, 6) : 1d; private LogTime() { // Utility class. } /** * Returns the current time in either millis or nanos depending on the api level to be used with * {@link #getElapsedMillis(long)}. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static long getLogTime() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return SystemClock.elapsedRealtimeNanos(); } else { return SystemClock.uptimeMillis(); } } /** * Returns the time elapsed since the given logTime in millis. * * @param logTime The start time of the event. */ public static double getElapsedMillis(long logTime) { return (getLogTime() - logTime) * MILLIS_MULTIPLIER; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/LruCache.java ================================================ package com.bumptech.glide.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; /** * A general purpose size limited cache that evicts items using an LRU algorithm. By default every * item is assumed to have a size of one. Subclasses can override {@link #getSize(Object)}} to * change the size on a per item basis. * * @param The type of the keys. * @param The type of the values. */ public class LruCache { private final Map> cache = new LinkedHashMap<>(100, 0.75f, true); private final long initialMaxSize; private long maxSize; private long currentSize; /** * Constructor for LruCache. * * @param size The maximum size of the cache, the units must match the units used in {@link * #getSize(Object)}. */ public LruCache(long size) { this.initialMaxSize = size; this.maxSize = size; } /** * Sets a size multiplier that will be applied to the size provided in the constructor to put the * new size of the cache. If the new size is less than the current size, entries will be evicted * until the current size is less than or equal to the new size. * * @param multiplier The multiplier to apply. */ public synchronized void setSizeMultiplier(float multiplier) { if (multiplier < 0) { throw new IllegalArgumentException("Multiplier must be >= 0"); } maxSize = Math.round(initialMaxSize * multiplier); evict(); } /** * Returns the size of a given item, defaulting to one. The units must match those used in the * size passed in to the constructor. Subclasses can override this method to return sizes in * various units, usually bytes. * * @param item The item to get the size of. */ protected int getSize(@Nullable Y item) { return 1; } /** Returns the number of entries stored in cache. */ protected synchronized int getCount() { return cache.size(); } /** * A callback called whenever an item is evicted from the cache. Subclasses can override. * * @param key The key of the evicted item. * @param item The evicted item. */ protected void onItemEvicted(@NonNull T key, @Nullable Y item) { // optional override } /** Returns the current maximum size of the cache in bytes. */ public synchronized long getMaxSize() { return maxSize; } /** Returns the sum of the sizes of all items in the cache. */ public synchronized long getCurrentSize() { return currentSize; } /** * Returns true if there is a value for the given key in the cache. * * @param key The key to check. */ public synchronized boolean contains(@NonNull T key) { return cache.containsKey(key); } /** * Returns the item in the cache for the given key or null if no such item exists. * * @param key The key to check. */ @Nullable public synchronized Y get(@NonNull T key) { Entry entry = cache.get(key); return entry != null ? entry.value : null; } /** * Adds the given item to the cache with the given key and returns any previous entry for the * given key that may have already been in the cache. * *

    If the size of the item is larger than the total cache size, the item will not be added to * the cache and instead {@link #onItemEvicted(Object, Object)} will be called synchronously with * the given key and item. * *

    The size of the item is determined by the {@link #getSize(Object)} method. To avoid errors * where {@link #getSize(Object)} returns different values for the same object when called at * different times, the size value is acquired in {@code put} and retained until the item is * evicted, replaced or removed. * *

    If {@code item} is null the behavior here is a little odd. For the most part it's similar to * simply calling {@link #remove(Object)} with the given key. The difference is that calling this * method with a null {@code item} will result in an entry remaining in the cache with a null * value and 0 size. The only real consequence is that at some point {@link #onItemEvicted(Object, * Object)} may be called with the given {@code key} and a null value. Ideally we'd make calling * this method with a null {@code item} identical to {@link #remove(Object)} but we're preserving * this odd behavior to match older versions :(. * * @param key The key to add the item at. * @param item The item to add. */ @Nullable public synchronized Y put(@NonNull T key, @Nullable Y item) { final int itemSize = getSize(item); if (itemSize >= maxSize) { onItemEvicted(key, item); return null; } if (item != null) { currentSize += itemSize; } @Nullable Entry old = cache.put(key, item == null ? null : new Entry<>(item, itemSize)); if (old != null) { currentSize -= old.size; if (!old.value.equals(item)) { onItemEvicted(key, old.value); } } evict(); return old != null ? old.value : null; } /** * Removes the item at the given key and returns the removed item if present, and null otherwise. * * @param key The key to remove the item at. */ @Nullable public synchronized Y remove(@NonNull T key) { Entry entry = cache.remove(key); if (entry == null) { return null; } currentSize -= entry.size; return entry.value; } /** Clears all items in the cache. */ public void clearMemory() { trimToSize(0); } /** * Removes the least recently used items from the cache until the current size is less than the * given size. * * @param size The size the cache should be less than. */ protected synchronized void trimToSize(long size) { Map.Entry> last; Iterator>> cacheIterator; while (currentSize > size) { cacheIterator = cache.entrySet().iterator(); last = cacheIterator.next(); final Entry toRemove = last.getValue(); currentSize -= toRemove.size; final T key = last.getKey(); cacheIterator.remove(); onItemEvicted(key, toRemove.value); } } private void evict() { trimToSize(maxSize); } @Synthetic static final class Entry { final Y value; final int size; @Synthetic Entry(Y value, int size) { this.value = value; this.size = size; } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/MarkEnforcingInputStream.java ================================================ package com.bumptech.glide.util; import androidx.annotation.NonNull; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * Prevents {@link InputStream InputStreams} from overflowing their buffer by reading data past * their read limit. */ public class MarkEnforcingInputStream extends FilterInputStream { private static final int UNSET = Integer.MIN_VALUE; private static final int END_OF_STREAM = -1; private int availableBytes = UNSET; public MarkEnforcingInputStream(@NonNull InputStream in) { super(in); } @Override public synchronized void mark(int readLimit) { super.mark(readLimit); availableBytes = readLimit; } @Override public int read() throws IOException { if (getBytesToRead(1) == END_OF_STREAM) { return END_OF_STREAM; } int result = super.read(); updateAvailableBytesAfterRead(1 /* bytesRead */); return result; } @Override public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) throws IOException { int toRead = (int) getBytesToRead(byteCount); if (toRead == END_OF_STREAM) { return END_OF_STREAM; } int read = super.read(buffer, byteOffset, toRead); updateAvailableBytesAfterRead(read); return read; } @Override public synchronized void reset() throws IOException { super.reset(); availableBytes = UNSET; } @Override public long skip(long byteCount) throws IOException { long toSkip = getBytesToRead(byteCount); if (toSkip == END_OF_STREAM) { return 0; } long read = super.skip(toSkip); updateAvailableBytesAfterRead(read); return read; } @Override public int available() throws IOException { return availableBytes == UNSET ? super.available() : Math.min(availableBytes, super.available()); } private long getBytesToRead(long targetByteCount) { if (availableBytes == 0) { return END_OF_STREAM; } else if (availableBytes != UNSET && targetByteCount > availableBytes) { return availableBytes; } else { return targetByteCount; } } private void updateAvailableBytesAfterRead(long bytesRead) { if (availableBytes != UNSET && bytesRead != END_OF_STREAM) { // See https://errorprone.info/bugpattern/NarrowingCompoundAssignment. availableBytes = (int) (availableBytes - bytesRead); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/MultiClassKey.java ================================================ package com.bumptech.glide.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** A key of two {@link Class}es to be used in hashed collections. */ @SuppressWarnings({"PMD.ConstructorCallsOverridableMethod"}) public class MultiClassKey { private Class first; private Class second; private Class third; public MultiClassKey() { // leave them null } public MultiClassKey(@NonNull Class first, @NonNull Class second) { set(first, second); } public MultiClassKey( @NonNull Class first, @NonNull Class second, @Nullable Class third) { set(first, second, third); } public void set(@NonNull Class first, @NonNull Class second) { set(first, second, null); } public void set(@NonNull Class first, @NonNull Class second, @Nullable Class third) { this.first = first; this.second = second; this.third = third; } @Override public String toString() { return "MultiClassKey{" + "first=" + first + ", second=" + second + '}'; } @SuppressWarnings({"PMD.SimplifyBooleanReturns", "RedundantIfStatement"}) @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MultiClassKey that = (MultiClassKey) o; if (!first.equals(that.first)) { return false; } if (!second.equals(that.second)) { return false; } if (!Util.bothNullOrEqual(third, that.third)) { return false; } return true; } @Override public int hashCode() { int result = first.hashCode(); result = 31 * result + second.hashCode(); result = 31 * result + (third != null ? third.hashCode() : 0); return result; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/Preconditions.java ================================================ package com.bumptech.glide.util; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Collection; /** Contains common assertions. */ public final class Preconditions { private Preconditions() { // Utility class. } public static void checkArgument(boolean expression) { checkArgument(expression, /* message= */ ""); } public static void checkArgument(boolean expression, @NonNull String message) { if (!expression) { throw new IllegalArgumentException(message); } } @NonNull public static T checkNotNull(@Nullable T arg) { return checkNotNull(arg, "Argument must not be null"); } @NonNull public static T checkNotNull(@Nullable T arg, @NonNull String message) { if (arg == null) { throw new NullPointerException(message); } return arg; } @NonNull public static String checkNotEmpty(@Nullable String string) { if (TextUtils.isEmpty(string)) { throw new IllegalArgumentException("Must not be null or empty"); } return string; } @NonNull public static , Y> T checkNotEmpty(@NonNull T collection) { if (collection.isEmpty()) { throw new IllegalArgumentException("Must not be empty."); } return collection; } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/Synthetic.java ================================================ package com.bumptech.glide.util; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** Indicates that target's visibility can be relaxed to avoid synthetic methods. */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) public @interface Synthetic {} ================================================ FILE: library/src/main/java/com/bumptech/glide/util/Util.java ================================================ package com.bumptech.glide.util; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.os.Build; import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.model.Model; import com.bumptech.glide.request.BaseRequestOptions; import com.bumptech.glide.request.target.Target; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Queue; /** A collection of assorted utility classes. */ public final class Util { private static final int HASH_MULTIPLIER = 31; private static final int HASH_ACCUMULATOR = 17; private static final char[] HEX_CHAR_ARRAY = "0123456789abcdef".toCharArray(); // 32 bytes from sha-256 -> 64 hex chars. private static final char[] SHA_256_CHARS = new char[64]; @Nullable private static volatile Handler mainThreadHandler; private Util() { // Utility class. } /** Returns the hex string of the given byte array representing a SHA256 hash. */ @NonNull public static String sha256BytesToHex(@NonNull byte[] bytes) { synchronized (SHA_256_CHARS) { return bytesToHex(bytes, SHA_256_CHARS); } } // Taken from: // http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java // /9655275#9655275 @SuppressWarnings("PMD.UseVarargs") @NonNull private static String bytesToHex(@NonNull byte[] bytes, @NonNull char[] hexChars) { int v; for (int j = 0; j < bytes.length; j++) { v = bytes[j] & 0xFF; hexChars[j * 2] = HEX_CHAR_ARRAY[v >>> 4]; hexChars[j * 2 + 1] = HEX_CHAR_ARRAY[v & 0x0F]; } return new String(hexChars); } /** * Returns the allocated byte size of the given bitmap. * * @see #getBitmapByteSize(android.graphics.Bitmap) * @deprecated Use {@link #getBitmapByteSize(android.graphics.Bitmap)} instead. Scheduled to be * removed in Glide 4.0. */ @Deprecated public static int getSize(@NonNull Bitmap bitmap) { return getBitmapByteSize(bitmap); } /** Returns the in memory size of the given {@link Bitmap} in bytes. */ @TargetApi(Build.VERSION_CODES.KITKAT) public static int getBitmapByteSize(@NonNull Bitmap bitmap) { // The return value of getAllocationByteCount silently changes for recycled bitmaps from the // internal buffer size to row bytes * height. To avoid random inconsistencies in caches, we // instead assert here. if (bitmap.isRecycled()) { throw new IllegalStateException( "Cannot obtain size for recycled Bitmap: " + bitmap + "[" + bitmap.getWidth() + "x" + bitmap.getHeight() + "] " + bitmap.getConfig()); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // Workaround for KitKat initial release NPE in Bitmap, fixed in MR1. See issue #148. try { return bitmap.getAllocationByteCount(); } catch ( @SuppressWarnings("PMD.AvoidCatchingNPE") NullPointerException e) { // Do nothing. } } return bitmap.getHeight() * bitmap.getRowBytes(); } /** * Returns the in memory size of {@link android.graphics.Bitmap} with the given width, height, and * {@link android.graphics.Bitmap.Config}. */ public static int getBitmapByteSize(int width, int height, @Nullable Bitmap.Config config) { return width * height * getBytesPerPixel(config); } /** * Returns the number of bytes required to store each pixel of a {@link Bitmap} with the given * {@code config}. * *

    Defaults to {@link Bitmap.Config#ARGB_8888} if {@code config} is {@code null}. */ public static int getBytesPerPixel(@Nullable Bitmap.Config config) { // A bitmap by decoding a GIF has null "config" in certain environments. if (config == null) { config = Bitmap.Config.ARGB_8888; } int bytesPerPixel; switch (config) { case ALPHA_8: bytesPerPixel = 1; break; case RGB_565: case ARGB_4444: bytesPerPixel = 2; break; case RGBA_F16: bytesPerPixel = 8; break; case ARGB_8888: default: bytesPerPixel = 4; break; } return bytesPerPixel; } /** * Returns {@code true} if {@code width} and {@code height} are both {@code > 0} and/or equal to * {@link Target#SIZE_ORIGINAL}. */ public static boolean isValidDimensions(int width, int height) { return isValidDimension(width) && isValidDimension(height); } public static boolean isValidDimension(int dimen) { return dimen > 0 || dimen == Target.SIZE_ORIGINAL; } /** Posts the given {@code runnable} to the UI thread using a shared {@link Handler}. */ public static void postOnUiThread(Runnable runnable) { getUiThreadHandler().post(runnable); } /** * Posts the given {@code runnable} to the front of the queue on the UI thread using a shared * {@link Handler}. */ public static void postAtFrontOfQueueOnUiThread(Runnable runnable) { getUiThreadHandler().postAtFrontOfQueue(runnable); } /** Removes the given {@code runnable} from the UI threads queue if it is still queued. */ public static void removeCallbacksOnUiThread(Runnable runnable) { getUiThreadHandler().removeCallbacks(runnable); } private static Handler getUiThreadHandler() { if (mainThreadHandler == null) { synchronized (Util.class) { if (mainThreadHandler == null) { mainThreadHandler = new Handler(Looper.getMainLooper()); } } } return mainThreadHandler; } /** * Throws an {@link java.lang.IllegalArgumentException} if called on a thread other than the main * thread. */ public static void assertMainThread() { if (!isOnMainThread()) { throw new IllegalArgumentException("You must call this method on the main thread"); } } /** Throws an {@link java.lang.IllegalArgumentException} if called on the main thread. */ public static void assertBackgroundThread() { if (!isOnBackgroundThread()) { throw new IllegalArgumentException("You must call this method on a background thread"); } } /** Returns {@code true} if called on the main thread, {@code false} otherwise. */ public static boolean isOnMainThread() { return Looper.myLooper() == Looper.getMainLooper(); } /** Returns {@code true} if called on a background thread, {@code false} otherwise. */ public static boolean isOnBackgroundThread() { return !isOnMainThread(); } /** Creates a {@link java.util.Queue} of the given size using Glide's preferred implementation. */ @NonNull public static Queue createQueue(int size) { return new ArrayDeque<>(size); } /** * Returns a copy of the given list that is safe to iterate over and perform actions that may * modify the original list. * *

    See #303, #375, #322, #2262. */ @NonNull @SuppressWarnings("UseBulkOperation") public static List getSnapshot(@NonNull Collection other) { // toArray creates a new ArrayList internally and does not guarantee that the values it contains // are non-null. Collections.addAll in ArrayList uses toArray internally and therefore also // doesn't guarantee that entries are non-null. WeakHashMap's iterator does avoid returning null // and is therefore safe to use. See #322, #2262. List result = new ArrayList<>(other.size()); for (T item : other) { if (item != null) { result.add(item); } } return result; } /** * Null-safe equivalent of {@code a.equals(b)}. * * @see java.util.Objects#equals */ public static boolean bothNullOrEqual(@Nullable Object a, @Nullable Object b) { return a == null ? b == null : a.equals(b); } public static boolean bothModelsNullEquivalentOrEquals(@Nullable Object a, @Nullable Object b) { if (a == null) { return b == null; } if (a instanceof Model) { return ((Model) a).isEquivalentTo(b); } return a.equals(b); } public static boolean bothBaseRequestOptionsNullEquivalentOrEquals( @Nullable BaseRequestOptions a, @Nullable BaseRequestOptions b) { if (a == null) { return b == null; } return a.isEquivalentTo(b); } public static int hashCode(int value) { return hashCode(value, HASH_ACCUMULATOR); } public static int hashCode(int value, int accumulator) { return accumulator * HASH_MULTIPLIER + value; } public static int hashCode(float value) { return hashCode(value, HASH_ACCUMULATOR); } public static int hashCode(float value, int accumulator) { return hashCode(Float.floatToIntBits(value), accumulator); } public static int hashCode(@Nullable Object object, int accumulator) { return hashCode(object == null ? 0 : object.hashCode(), accumulator); } public static int hashCode(boolean value, int accumulator) { return hashCode(value ? 1 : 0, accumulator); } public static int hashCode(boolean value) { return hashCode(value, HASH_ACCUMULATOR); } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/ViewPreloadSizeProvider.java ================================================ package com.bumptech.glide.util; import android.graphics.drawable.Drawable; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.ListPreloader; import com.bumptech.glide.request.target.CustomViewTarget; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.transition.Transition; import java.util.Arrays; /** * A {@link com.bumptech.glide.ListPreloader.PreloadSizeProvider} that will extract the preload size * from a given {@link android.view.View}. * * @param The type of the model the size should be provided for. */ public class ViewPreloadSizeProvider implements ListPreloader.PreloadSizeProvider, SizeReadyCallback { private int[] size; // We need to keep a strong reference to the Target so that it isn't garbage collected due to a // weak reference // while we're waiting to get its size. @SuppressWarnings("unused") private SizeViewTarget viewTarget; /** * Constructor that does nothing by default and requires users to call {@link * #setView(android.view.View)} when a View is available to registerComponents the dimensions * returned by this class. */ public ViewPreloadSizeProvider() { // This constructor is intentionally empty. Nothing special is needed here. } /** * Constructor that will extract the preload size from a given {@link android.view.View}. * * @param view A not null View the size will be extracted from async using an {@link * android.view.ViewTreeObserver .OnPreDrawListener} */ // Public API. @SuppressWarnings("WeakerAccess") public ViewPreloadSizeProvider(@NonNull View view) { viewTarget = new SizeViewTarget(view); viewTarget.getSize(this); } @Nullable @Override public int[] getPreloadSize(@NonNull T item, int adapterPosition, int itemPosition) { if (size == null) { return null; } else { return Arrays.copyOf(size, size.length); } } @Override public void onSizeReady(int width, int height) { size = new int[] {width, height}; viewTarget = null; } /** * Sets the {@link android.view.View} the size will be extracted. * *

    Note - only the first call to this method will be obeyed, subsequent requests will be * ignored. * * @param view A not null View the size will be extracted async with an {@link * android.view.ViewTreeObserver .OnPreDrawListener} */ public void setView(@NonNull View view) { if (size != null || viewTarget != null) { return; } viewTarget = new SizeViewTarget(view); viewTarget.getSize(this); } static final class SizeViewTarget extends CustomViewTarget { SizeViewTarget(@NonNull View view) { super(view); } @Override protected void onResourceCleared(@Nullable Drawable placeholder) {} @Override public void onLoadFailed(@Nullable Drawable errorDrawable) {} @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) {} } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/pool/FactoryPools.java ================================================ package com.bumptech.glide.util.pool; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.util.Pools.Pool; import androidx.core.util.Pools.SimplePool; import androidx.core.util.Pools.SynchronizedPool; import java.util.ArrayList; import java.util.List; /** * Provides implementations of {@link Pool} never return {@code null}, log when new instances are * created, and that can use the {@link com.bumptech.glide.util.pool.FactoryPools.Poolable} * interface to ensure objects aren't used while inside the pool. */ public final class FactoryPools { private static final String TAG = "FactoryPools"; private static final int DEFAULT_POOL_SIZE = 20; private static final Resetter EMPTY_RESETTER = new Resetter() { @Override public void reset(@NonNull Object object) { // Do nothing. } }; private FactoryPools() {} /** * Returns a non-thread safe {@link Pool} that never returns {@code null} from {@link * Pool#acquire()} and that contains objects of the type created by the given {@link Factory} with * the given maximum size. * *

    If the pool is empty when {@link Pool#acquire()} is called, the given {@link Factory} will * be used to create a new instance. * * @param The type of object the pool will contains. */ @NonNull public static Pool simple(int size, @NonNull Factory factory) { return build(new SimplePool(size), factory); } /** * Identical to {@link #threadSafe(int, Factory, Resetter)} except no action is taken when an * instance is returned to the pool. */ @NonNull public static Pool threadSafe(int size, @NonNull Factory factory) { return build(new SynchronizedPool(size), factory); } /** * Returns a new thread safe {@link Pool} that never returns {@code null} from {@link * Pool#acquire()} and that contains objects of the type created by the given {@link Factory} with * the given maximum size. * *

    If the pool is empty when {@link Pool#acquire()} is called, the given {@link Factory} will * be used to create a new instance. * *

    Each time an instance is returned to the pool {@code resetter} will be called with the given * instance. * * @param The type of object the pool will contains. */ @NonNull public static Pool threadSafe( int size, @NonNull Factory factory, @NonNull Resetter resetter) { return build(new SynchronizedPool(size), factory, resetter); } /** * Returns a new {@link Pool} that never returns {@code null} and that contains {@link List Lists} * of a specific generic type with a standard maximum size of 20. * *

    If the pool is empty when {@link Pool#acquire()} is called, a new {@link List} will be * created. * * @param The type of object that the {@link List Lists} will contain. */ @NonNull public static Pool> threadSafeList() { return threadSafeList(DEFAULT_POOL_SIZE); } /** * Returns a new thread safe {@link Pool} that never returns {@code null} and that contains {@link * List Lists} of a specific generic type with the given maximum size. * *

    If the pool is empty when {@link Pool#acquire()} is called, a new {@link List} will be * created. * * @param The type of object that the {@link List Lists} will contain. */ // Public API. @SuppressWarnings("WeakerAccess") @NonNull public static Pool> threadSafeList(int size) { return build( new SynchronizedPool>(size), new Factory>() { @NonNull @Override public List create() { return new ArrayList<>(); } }, new Resetter>() { @Override public void reset(@NonNull List object) { object.clear(); } }); } @NonNull private static Pool build( @NonNull Pool pool, @NonNull Factory factory) { return build(pool, factory, FactoryPools.emptyResetter()); } @NonNull private static Pool build( @NonNull Pool pool, @NonNull Factory factory, @NonNull Resetter resetter) { return new FactoryPool<>(pool, factory, resetter); } @NonNull @SuppressWarnings("unchecked") private static Resetter emptyResetter() { return (Resetter) EMPTY_RESETTER; } /** * Creates new instances of the given type. * * @param The type of Object that will be created. */ public interface Factory { T create(); } /** * Resets state when objects are returned to the pool. * * @param The type of Object that will be reset. */ public interface Resetter { void reset(@NonNull T object); } /** * Allows additional verification to catch errors caused by using objects while they are in an * object pool. */ public interface Poolable { @NonNull StateVerifier getVerifier(); } private static final class FactoryPool implements Pool { private final Factory factory; private final Resetter resetter; private final Pool pool; FactoryPool(@NonNull Pool pool, @NonNull Factory factory, @NonNull Resetter resetter) { this.pool = pool; this.factory = factory; this.resetter = resetter; } @Override public T acquire() { T result = pool.acquire(); if (result == null) { result = factory.create(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Created new " + result.getClass()); } } if (result instanceof Poolable) { ((Poolable) result).getVerifier().setRecycled(false /*isRecycled*/); } return result; } @Override public boolean release(@NonNull T instance) { if (instance instanceof Poolable) { ((Poolable) instance).getVerifier().setRecycled(true /*isRecycled*/); } resetter.reset(instance); return pool.release(instance); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/pool/GlideTrace.java ================================================ package com.bumptech.glide.util.pool; import androidx.tracing.Trace; import java.util.concurrent.atomic.AtomicInteger; /** Systracing utilities for Glide. */ public final class GlideTrace { // Enable this locally to see tracing statements. private static final boolean TRACING_ENABLED = false; private static final AtomicInteger COOKIE_CREATOR = TRACING_ENABLED ? new AtomicInteger() : null; /** Maximum length of a systrace tag. */ private static final int MAX_LENGTH = 127; private GlideTrace() { // Utility class. } private static String truncateTag(String tag) { if (tag.length() > MAX_LENGTH) { return tag.substring(0, MAX_LENGTH - 1); } return tag; } public static void beginSection(String tag) { if (TRACING_ENABLED) { Trace.beginSection(truncateTag(tag)); } } public static void beginSectionFormat(String format, Object arg1) { if (TRACING_ENABLED) { Trace.beginSection(truncateTag(String.format(format, arg1))); } } public static void beginSectionFormat(String format, Object arg1, Object arg2) { if (TRACING_ENABLED) { Trace.beginSection(truncateTag(String.format(format, arg1, arg2))); } } public static void beginSectionFormat(String format, Object arg1, Object arg2, Object arg3) { if (TRACING_ENABLED) { Trace.beginSection(truncateTag(String.format(format, arg1, arg2, arg3))); } } public static int beginSectionAsync(String tag) { if (TRACING_ENABLED) { int cookie = COOKIE_CREATOR.incrementAndGet(); Trace.beginAsyncSection(truncateTag(tag), cookie); return cookie; } return -1; } public static void endSectionAsync(String tag, int cookie) { if (TRACING_ENABLED) { Trace.endAsyncSection(tag, cookie); } } public static void endSection() { if (TRACING_ENABLED) { Trace.endSection(); } } } ================================================ FILE: library/src/main/java/com/bumptech/glide/util/pool/StateVerifier.java ================================================ package com.bumptech.glide.util.pool; import androidx.annotation.NonNull; import com.bumptech.glide.util.Synthetic; /** Verifies that the job is not in the recycled state. */ public abstract class StateVerifier { private static final boolean DEBUG = false; /** Creates a new {@link StateVerifier} instance. */ @NonNull public static StateVerifier newInstance() { if (DEBUG) { return new DebugStateVerifier(); } else { return new DefaultStateVerifier(); } } private StateVerifier() {} /** * Throws an exception if we believe our object is recycled and inactive (i.e. is currently in an * object pool). */ public abstract void throwIfRecycled(); /** Sets whether or not our object is recycled. */ abstract void setRecycled(boolean isRecycled); private static class DefaultStateVerifier extends StateVerifier { private volatile boolean isReleased; @Synthetic DefaultStateVerifier() {} @Override public void throwIfRecycled() { if (isReleased) { throw new IllegalStateException("Already released"); } } @Override public void setRecycled(boolean isRecycled) { this.isReleased = isRecycled; } } private static class DebugStateVerifier extends StateVerifier { // Keeps track of the stack trace where our state was set to recycled. private volatile RuntimeException recycledAtStackTraceException; @Synthetic DebugStateVerifier() {} @Override public void throwIfRecycled() { if (recycledAtStackTraceException != null) { throw new IllegalStateException("Already released", recycledAtStackTraceException); } } @Override void setRecycled(boolean isRecycled) { if (isRecycled) { recycledAtStackTraceException = new RuntimeException("Released"); } else { recycledAtStackTraceException = null; } } } } ================================================ FILE: library/src/main/res/values/ids.xml ================================================ ================================================ FILE: library/src/test/java/com/bumptech/glide/request/target/CustomViewTargetTest.java ================================================ package com.bumptech.glide.request.target; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.transition.Transition; import com.google.common.truth.Truth; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; /** * Test for {@link CustomViewTarget}. * *

    TODO: This should really be in the tests subproject, but that causes errors because the R * class referenced in {@link CustomViewTarget} can't be found. This should be fixable with some * gradle changes, but I've so far failed to figure out the right set of commands. */ @RunWith(RobolectricTestRunner.class) @Config(sdk = Config.OLDEST_SDK) public class CustomViewTargetTest { private ActivityController activity; private View view; private ViewGroup parent; private CustomViewTarget target; @Mock private SizeReadyCallback cb; @Mock private Request request; private AttachStateTarget attachStateTarget; @Before public void setUp() { MockitoAnnotations.initMocks(this); activity = Robolectric.buildActivity(Activity.class).create().start().postCreate(null).resume(); view = new View(activity.get()); target = new TestViewTarget(view); attachStateTarget = new AttachStateTarget(view); LinearLayout linearLayout = new LinearLayout(activity.get()); View expandView = new View(activity.get()); LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, /* height= */ 0); linearLayoutParams.weight = 1f; expandView.setLayoutParams(linearLayoutParams); linearLayout.addView(expandView); parent = new FrameLayout(activity.get()); parent.addView(view); linearLayout.addView(parent); activity.get().setContentView(linearLayout); } @After public void tearDown() { CustomViewTarget.SizeDeterminer.maxDisplayLength = null; } @Test public void testReturnsWrappedView() { assertEquals(view, target.getView()); } @Test public void testReturnsNullFromGetRequestIfNoRequestSet() { assertNull(target.getRequest()); } @Test public void testCanSetAndRetrieveRequest() { target.setRequest(request); assertEquals(request, target.getRequest()); } @Test public void testRetrievesRequestFromPreviousTargetForView() { target.setRequest(request); CustomViewTarget second = new TestViewTarget(view); assertEquals(request, second.getRequest()); } @Test public void testSizeCallbackIsCalledSynchronouslyIfViewSizeSet() { int dimens = 333; // activity.get().setContentView(view); view.layout(0, 0, dimens, dimens); target.getSize(cb); verify(cb).onSizeReady(eq(dimens), eq(dimens)); } @Test public void testSizeCallbackIsCalledSynchronouslyIfLayoutParamsConcreteSizeSet() { int dimens = 444; LayoutParams layoutParams = new FrameLayout.LayoutParams(dimens, dimens); view.setLayoutParams(layoutParams); view.requestLayout(); target.getSize(cb); verify(cb).onSizeReady(eq(dimens), eq(dimens)); } @Config(qualifiers = "w200dp-h300dp") @Test public void getSize_withBothWrapContent_usesDisplayDimens() { LayoutParams layoutParams = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); view.setLayoutParams(layoutParams); activity.visible(); view.layout(0, 0, 0, 0); target.getSize(cb); verify(cb).onSizeReady(300, 300); } @Config(qualifiers = "w100dp-h200dp") @Test public void getSize_withWrapContentWidthAndValidHeight_usesDisplayDimenAndValidHeight() { int height = 100; LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, height); view.setLayoutParams(params); activity.visible(); view.setRight(0); target.getSize(cb); verify(cb).onSizeReady(200, height); } @Config(qualifiers = "w200dp-h100dp") @Test public void getSize_withWrapContentHeightAndValidWidth_returnsWidthAndDisplayDimen() { int width = 100; LayoutParams params = new FrameLayout.LayoutParams(width, LayoutParams.WRAP_CONTENT); view.setLayoutParams(params); parent.getLayoutParams().height = 200; activity.visible(); target.getSize(cb); verify(cb).onSizeReady(width, 200); } @Config(qualifiers = "w500dp-h600dp") @Test public void getSize_withWrapContentWidthAndMatchParentHeight_usesDisplayDimenWidthAndHeight() { LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); view.setLayoutParams(params); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int height = 32; parent.getLayoutParams().height = height; activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb).onSizeReady(500, height); } @Config(qualifiers = "w300dp-h400dp") @Test public void getSize_withMatchParentWidthAndWrapContentHeight_usesWidthAndDisplayDimenHeight() { LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); view.setLayoutParams(params); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int width = 32; parent.getLayoutParams().width = 32; activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); if (Build.VERSION.SDK_INT <= 19) { verify(cb).onSizeReady(width, 352); } else { verify(cb).onSizeReady(width, 344); } } @Test public void testMatchParentWidthAndHeight() { LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); view.setLayoutParams(params); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb).onSizeReady(eq(parent.getWidth()), eq(parent.getHeight())); } @Test public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParams() { target.getSize(cb); int width = 12; int height = 32; parent.getLayoutParams().width = width; parent.getLayoutParams().height = height; activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testSizeCallbacksAreCalledInOrderPreDraw() { SizeReadyCallback[] cbs = new SizeReadyCallback[25]; for (int i = 0; i < cbs.length; i++) { cbs[i] = mock(SizeReadyCallback.class); target.getSize(cbs[i]); } int width = 100; int height = 111; parent.getLayoutParams().width = width; parent.getLayoutParams().height = height; activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); InOrder order = inOrder((Object[]) cbs); for (SizeReadyCallback cb : cbs) { order.verify(cb).onSizeReady(eq(width), eq(height)); } } @Test public void testDoesNotNotifyCallbackTwiceIfAddedTwice() { target.getSize(cb); target.getSize(cb); view.setLayoutParams(new FrameLayout.LayoutParams(100, 100)); activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void testDoesNotAddMultipleListenersIfMultipleCallbacksAreAdded() { SizeReadyCallback cb1 = mock(SizeReadyCallback.class); SizeReadyCallback cb2 = mock(SizeReadyCallback.class); target.getSize(cb1); target.getSize(cb2); view.getViewTreeObserver().dispatchOnPreDraw(); // assertThat(shadowObserver.getPreDrawListeners()).hasSize(1); } @Test public void testDoesAddSecondListenerIfFirstListenerIsRemovedBeforeSecondRequest() { SizeReadyCallback cb1 = mock(SizeReadyCallback.class); target.getSize(cb1); view.setLayoutParams(new FrameLayout.LayoutParams(100, 100)); activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); SizeReadyCallback cb2 = mock(SizeReadyCallback.class); view.setLayoutParams( new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); target.getSize(cb2); view.setLayoutParams(new FrameLayout.LayoutParams(100, 100)); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb2).onSizeReady(anyInt(), anyInt()); } @Test public void testSizeCallbackIsNotCalledPreDrawIfNoDimensSetOnPreDraw() { target.getSize(cb); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb, never()).onSizeReady(anyInt(), anyInt()); activity.visible(); verify(cb).onSizeReady(anyInt(), anyInt()); } @Test public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParamsButLayoutParamsSetLater() { target.getSize(cb); int width = 689; int height = 354; LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height); view.setLayoutParams(layoutParams); view.requestLayout(); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testCallbackIsNotCalledTwiceIfPreDrawFiresTwice() { activity.visible(); target.getSize(cb); LayoutParams layoutParams = new FrameLayout.LayoutParams(1234, 4123); view.setLayoutParams(layoutParams); view.requestLayout(); view.getViewTreeObserver().dispatchOnPreDraw(); view.getViewTreeObserver().dispatchOnPreDraw(); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void testCallbacksFromMultipleRequestsAreNotifiedOnPreDraw() { SizeReadyCallback firstCb = mock(SizeReadyCallback.class); SizeReadyCallback secondCb = mock(SizeReadyCallback.class); target.getSize(firstCb); target.getSize(secondCb); int width = 68; int height = 875; LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height); view.setLayoutParams(layoutParams); activity.visible(); view.getViewTreeObserver().dispatchOnPreDraw(); view.getViewTreeObserver().dispatchOnPreDraw(); verify(firstCb, times(1)).onSizeReady(eq(width), eq(height)); verify(secondCb, times(1)).onSizeReady(eq(width), eq(height)); } @Test public void testDoesNotThrowOnPreDrawIfViewTreeObserverIsDead() { target.getSize(cb); int width = 1; int height = 2; LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height); view.setLayoutParams(layoutParams); ViewTreeObserver vto = view.getViewTreeObserver(); view.requestLayout(); activity.visible(); assertFalse(vto.isAlive()); vto.dispatchOnPreDraw(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullView() { new TestViewTarget(null); } @Test public void testDecreasesDimensionsByViewPadding() { activity.visible(); view.setLayoutParams(new FrameLayout.LayoutParams(100, 100)); view.setPadding(25, 25, 25, 25); view.requestLayout(); target.getSize(cb); verify(cb).onSizeReady(50, 50); } @Test public void getSize_withValidWidthAndHeight_notLaidOut_notLayoutRequested_callsSizeReady() { view.setRight(100); view.setBottom(100); target.getSize(cb); verify(cb).onSizeReady(100, 100); } @Test public void getSize_withLayoutParams_notLaidOut_doesCallSizeReady() { view.setLayoutParams(new FrameLayout.LayoutParams(10, 10)); view.setRight(100); view.setBottom(100); target.getSize(cb); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void getSize_withLayoutParams_emptyParams_notLaidOutOrLayoutRequested_callsSizeReady() { view.setLayoutParams(new FrameLayout.LayoutParams(0, 0)); view.setRight(100); view.setBottom(100); target.getSize(cb); verify(cb).onSizeReady(100, 100); } @Test public void getSize_withValidWidthAndHeight_preV19_layoutRequested_callsSizeReady() { view.setLayoutParams(new FrameLayout.LayoutParams(100, 100)); view.requestLayout(); target.getSize(cb); verify(cb).onSizeReady(100, 100); } @Test public void getSize_withWidthAndHeightEqualToPadding_doesNotCallSizeReady() { view.setLayoutParams(new FrameLayout.LayoutParams(100, 100)); view.requestLayout(); view.setPadding(50, 50, 50, 50); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); } @Test public void clearOnDetach_onDetach_withNullRequest_doesNothing() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(null); activity.visible(); } // This behavior isn't clearly correct, but it doesn't seem like there's any harm to clear an // already cleared request, so we might as well avoid the extra check/complexity in the code. @Test public void clearOnDetach_onDetach_withClearedRequest_clearsRequest() { activity.visible(); attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); parent.removeView(view); verify(request).clear(); } @Test public void clearOnDetach_onDetach_withRunningRequest_pausesRequestOnce() { activity.visible(); attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); parent.removeView(view); verify(request).clear(); } @Test public void clearOnDetach_onDetach_afterOnLoadCleared_removesListener() { activity.visible(); attachStateTarget.clearOnDetach(); attachStateTarget.onLoadCleared(/* placeholder= */ null); attachStateTarget.setRequest(request); parent.removeView(view); verify(request, never()).clear(); } @Test public void clearOnDetach_moreThanOnce_registersObserverOnce() { activity.visible(); attachStateTarget.setRequest(request); attachStateTarget.clearOnDetach().clearOnDetach(); parent.removeView(view); verify(request).clear(); } @Test public void clearOnDetach_onDetach_afterMultipleClearOnDetaches_removesListener() { activity.visible(); attachStateTarget.clearOnDetach().clearOnDetach().clearOnDetach(); attachStateTarget.onLoadCleared(/* placeholder= */ null); attachStateTarget.setRequest(request); parent.removeView(view); verify(request, never()).clear(); } @Test public void clearOnDetach_onDetach_afterLoadCleared_clearsRequest() { activity.visible(); attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); parent.removeView(view); verify(request).clear(); } @Test public void clearOnDetach_onAttach_withNullRequest_doesNothing() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(null); activity.visible(); } @Test public void clearOnDetach_onAttach_withRunningRequest_doesNotBeginRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(false); activity.visible(); verify(request, never()).begin(); } @Test public void clearOnDetach_onAttach_withClearedRequest_beginsRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); activity.visible(); verify(request).begin(); } @Test public void clearOnDetach_afterLoadClearedAndRestarted_onAttach_beginsRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); attachStateTarget.onLoadCleared(/* placeholder= */ null); attachStateTarget.onLoadStarted(/* placeholder= */ null); activity.visible(); verify(request).begin(); } @Test public void clearOnDetach_onAttach_afterLoadCleared_doesNotBeingRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); attachStateTarget.onLoadCleared(/* placeholder= */ null); activity.visible(); verify(request, never()).begin(); } @Test public void onLoadStarted_withoutClearOnDetach_doesNotAddListener() { activity.visible(); target.setRequest(request); attachStateTarget.onLoadStarted(/* placeholder= */ null); parent.removeView(view); verify(request, never()).clear(); } @Test public void onLoadCleared_withoutClearOnDetach_doesNotRemoveListeners() { final AtomicInteger count = new AtomicInteger(); OnAttachStateChangeListener expected = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { count.incrementAndGet(); } @Override public void onViewDetachedFromWindow(View v) { // Intentionally Empty. } }; view.addOnAttachStateChangeListener(expected); attachStateTarget.onLoadCleared(/* placeholder= */ null); activity.visible(); Truth.assertThat(count.get()).isEqualTo(1); } private static final class AttachStateTarget extends CustomViewTarget { AttachStateTarget(View view) { super(view); } @Override protected void onResourceCleared(@Nullable Drawable placeholder) { // Intentionally Empty. } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { // Intentionally Empty. } @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) { // Intentionally Empty. } } private static final class TestViewTarget extends CustomViewTarget { TestViewTarget(View view) { super(view); } @Override protected void onResourceCleared(@Nullable Drawable placeholder) { // Intentionally Empty. } // We're intentionally avoiding the super call. @SuppressWarnings("MissingSuperCall") @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) { // Avoid calling super. } // We're intentionally avoiding the super call. @SuppressWarnings("MissingSuperCall") @Override public void onResourceLoading(@Nullable Drawable placeholder) { // Avoid calling super. } // We're intentionally avoiding the super call. @SuppressWarnings("MissingSuperCall") @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { // Avoid calling super. } } } ================================================ FILE: library/test/build.gradle.kts ================================================ import com.android.build.gradle.LibraryExtension import org.gradle.api.JavaVersion import org.gradle.api.tasks.compile.JavaCompile plugins { id("com.android.library") } tasks.withType().configureEach { options.setFork(true) } android { (this as LibraryExtension).testOptions.unitTests.all { testTask -> testTask.maxHeapSize = rootProject.extra.get("TEST_JVM_MEMORY_SIZE") as String if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { testTask.jvmArgs("-XX:MaxPermSize=${rootProject.extra.get("TEST_JVM_MEMORY_SIZE")}") } testTask.maxParallelForks = 2 } namespace = "com.bumptech.glide.test" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } testOptions.unitTests.isIncludeAndroidResources = true sourceSets { getByName("androidTest") { resources.srcDirs(files("../../exifsamples")) } getByName("test") { resources.srcDirs(files("../../exifsamples")) } } } dependencies { testImplementation(libs.androidx.appcompat) testImplementation(project(":library")) testImplementation(project(":mocks")) testImplementation(project(":testutil")) testImplementation(libs.guava.testlib) testImplementation(libs.truth) testImplementation(libs.junit) testImplementation(libs.mockito.core) testImplementation(libs.robolectric) testImplementation(libs.mockwebserver) testImplementation(libs.androidx.test.core) testImplementation(libs.androidx.junit) testImplementation(libs.androidx.test.runner) } afterEvaluate { tasks.named("lint").configure { enabled = false } tasks.named("compileReleaseJavaWithJavac").configure { enabled = false } tasks.named("compileDebugJavaWithJavac").configure { enabled = false } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/GlideContextTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import android.app.Application; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.Log; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide.RequestOptionsFactory; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.ImageViewTargetFactory; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public final class GlideContextTest { private Map, TransitionOptions> transitionOptions; private GlideContext context; @Before public void setUp() { Application app = ApplicationProvider.getApplicationContext(); transitionOptions = new HashMap<>(); context = new GlideContext( app, new LruArrayPool(), new GlideSupplier() { @Override public Registry get() { return new Registry(); } }, new ImageViewTargetFactory(), new RequestOptionsFactory() { @NonNull @Override public RequestOptions build() { return new RequestOptions(); } }, transitionOptions, /* defaultRequestListeners= */ Collections.>emptyList(), mock(Engine.class), mock(GlideExperiments.class), Log.DEBUG); } @Test public void getDefaultTransitionOptions_withNoOptionsRegistered_returnsDefaultOptions() { assertThat(context.getDefaultTransitionOptions(Object.class)) .isEqualTo(GlideContext.DEFAULT_TRANSITION_OPTIONS); } @Test public void getDefaultTransitionOptions_withNonMatchingOptionRegistered_returnsDefaultOptions() { transitionOptions.put(Bitmap.class, new GenericTransitionOptions<>()); assertThat(context.getDefaultTransitionOptions(Drawable.class)) .isEqualTo(GlideContext.DEFAULT_TRANSITION_OPTIONS); } @Test public void getDefaultTransitionOptions_withMatchingOptionsRegistered_returnsMatchingOptions() { GenericTransitionOptions expected = new GenericTransitionOptions<>(); transitionOptions.put(Bitmap.class, expected); assertThat(context.getDefaultTransitionOptions(Bitmap.class)).isEqualTo(expected); } @Test public void getDefaultTransitionOptions_withSuperClassRegistered_returnsSuperClassOptions() { DrawableTransitionOptions expected = new DrawableTransitionOptions(); transitionOptions.put(Drawable.class, expected); assertThat(context.getDefaultTransitionOptions(BitmapDrawable.class)).isEqualTo(expected); assertThat(context.getDefaultTransitionOptions(GifDrawable.class)).isEqualTo(expected); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/GlideTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.request.RequestOptions.decodeTypeOf; import static com.bumptech.glide.request.RequestOptions.errorOf; import static com.bumptech.glide.request.RequestOptions.placeholderOf; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.manager.Lifecycle; import com.bumptech.glide.manager.RequestManagerTreeNode; import com.bumptech.glide.module.GlideModule; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.tests.TearDownGlide; import com.bumptech.glide.tests.Util; import com.bumptech.glide.testutil.TestResourceUtil; import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; /** Tests for the {@link Glide} interface and singleton. */ @LooperMode(LEGACY) @RunWith(RobolectricTestRunner.class) @Config( sdk = ROBOLECTRIC_SDK, shadows = { GlideTest.ShadowFileDescriptorContentResolver.class, }) @SuppressWarnings("unchecked") public class GlideTest { // Fixes method overload confusion. private static final Object NULL = null; @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); @SuppressWarnings("rawtypes") @Mock private Target target; @Mock private DiskCache.Factory diskCacheFactory; @Mock private DiskCache diskCache; @Mock private MemoryCache memoryCache; @Mock private Lifecycle lifecycle; @Mock private RequestManagerTreeNode treeNode; @Mock private BitmapPool bitmapPool; private ImageView imageView; private RequestManager requestManager; private Context context; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); // Run all tasks on the main thread so they complete synchronously. GlideExecutor executor = MockGlideExecutor.newMainThreadExecutor(); when(diskCacheFactory.build()).thenReturn(diskCache); Glide.init( context, new GlideBuilder() .setMemoryCache(memoryCache) .setDiskCache(diskCacheFactory) .setSourceExecutor(executor) .setDiskCacheExecutor(executor)); Registry registry = Glide.get(context).getRegistry(); registerMockModelLoader( GlideUrl.class, InputStream.class, new ByteArrayInputStream(new byte[0]), registry); registerMockModelLoader( File.class, InputStream.class, new ByteArrayInputStream(new byte[0]), registry); registerMockModelLoader( File.class, ParcelFileDescriptor.class, mock(ParcelFileDescriptor.class), registry); registerMockModelLoader(File.class, ByteBuffer.class, ByteBuffer.allocate(10), registry); // Ensure that target's size ready callback will be called synchronously. imageView = new ImageView(context); imageView.setLayoutParams(new ViewGroup.LayoutParams(100, 100)); imageView.layout(0, 0, 100, 100); doAnswer(new CallSizeReady()).when(target).getSize(isA(SizeReadyCallback.class)); requestManager = new RequestManager(Glide.get(context), lifecycle, treeNode, context); requestManager.resumeRequests(); } @Test public void testCanSetMemoryCategory() { MemoryCategory memoryCategory = MemoryCategory.NORMAL; Glide glide = buildGlideWithFakePools(); glide.setMemoryCategory(memoryCategory); verify(memoryCache).setSizeMultiplier(eq(memoryCategory.getMultiplier())); verify(bitmapPool).setSizeMultiplier(eq(memoryCategory.getMultiplier())); } @Test public void testCanIncreaseMemoryCategory() { MemoryCategory memoryCategory = MemoryCategory.NORMAL; Glide glide = buildGlideWithFakePools(); glide.setMemoryCategory(memoryCategory); verify(memoryCache).setSizeMultiplier(eq(memoryCategory.getMultiplier())); verify(bitmapPool).setSizeMultiplier(eq(memoryCategory.getMultiplier())); MemoryCategory newMemoryCategory = MemoryCategory.HIGH; MemoryCategory oldMemoryCategory = glide.setMemoryCategory(newMemoryCategory); assertEquals(memoryCategory, oldMemoryCategory); verify(memoryCache).setSizeMultiplier(eq(newMemoryCategory.getMultiplier())); verify(bitmapPool).setSizeMultiplier(eq(newMemoryCategory.getMultiplier())); } @Test public void testCanDecreaseMemoryCategory() { MemoryCategory memoryCategory = MemoryCategory.NORMAL; Glide glide = buildGlideWithFakePools(); glide.setMemoryCategory(memoryCategory); verify(memoryCache).setSizeMultiplier(eq(memoryCategory.getMultiplier())); verify(bitmapPool).setSizeMultiplier(eq(memoryCategory.getMultiplier())); MemoryCategory newMemoryCategory = MemoryCategory.LOW; MemoryCategory oldMemoryCategory = glide.setMemoryCategory(newMemoryCategory); assertEquals(memoryCategory, oldMemoryCategory); verify(memoryCache).setSizeMultiplier(eq(newMemoryCategory.getMultiplier())); verify(bitmapPool).setSizeMultiplier(eq(newMemoryCategory.getMultiplier())); } @Test public void testClearMemory() { Glide glide = buildGlideWithFakePools(); glide.clearMemory(); verify(bitmapPool).clearMemory(); verify(memoryCache).clearMemory(); } @Test public void testTrimMemory() { Glide glide = buildGlideWithFakePools(); final int level = 123; glide.trimMemory(level); verify(bitmapPool).trimMemory(eq(level)); verify(memoryCache).trimMemory(eq(level)); } private Glide buildGlideWithFakePools() { return new GlideBuilder() .setBitmapPool(bitmapPool) .setMemoryCache(memoryCache) .build( context, Collections.emptyList(), /* annotationGeneratedGlideModule= */ null); } @Test public void testFileDefaultLoaderWithInputStream() { registerFailFactory(File.class, ParcelFileDescriptor.class); runTestFileDefaultLoader(); } @Test public void testFileDefaultLoaderWithFileDescriptor() { registerFailFactory(File.class, InputStream.class); runTestFileDefaultLoader(); } @Test public void testFileDefaultLoader() { runTestFileDefaultLoader(); } private void runTestFileDefaultLoader() { File file = new File("fake"); mockUri(Uri.fromFile(file)); requestManager.load(file).into(target); requestManager.load(file).into(imageView); verify(target).onResourceReady(isA(BitmapDrawable.class), isA(Transition.class)); verify(target).setRequest((Request) notNull()); assertNotNull(imageView.getDrawable()); } @SuppressWarnings("deprecation") @Test public void testUrlDefaultLoader() throws MalformedURLException { URL url = new URL("http://www.google.com"); requestManager.load(url).into(target); requestManager.load(url).into(imageView); verify(target).onResourceReady(isA(BitmapDrawable.class), isA(Transition.class)); verify(target).setRequest((Request) notNull()); assertNotNull(imageView.getDrawable()); } @Test public void testAsBitmapOption() { Uri uri = Uri.parse("content://something/else"); mockUri(uri); requestManager.asBitmap().load(uri).into(target); verify(target).onResourceReady(isA(Bitmap.class), isA(Transition.class)); } @Test public void testToBytesOption() { Uri uri = Uri.parse("content://something/else"); mockUri(uri); requestManager.as(byte[].class).apply(decodeTypeOf(Bitmap.class)).load(uri).into(target); verify(target).onResourceReady(isA(byte[].class), isA(Transition.class)); } @Test public void testLoadColorDrawable_withUnitBitmapTransformation_returnsColorDrawable() { ColorDrawable colorDrawable = new ColorDrawable(Color.RED); requestManager .load(colorDrawable) .apply(new RequestOptions().override(100, 100).centerCrop()) .into(target); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); verify(target).onResourceReady(argumentCaptor.capture(), isA(Transition.class)); Object result = argumentCaptor.getValue(); assertThat(result).isInstanceOf(ColorDrawable.class); assertThat(((ColorDrawable) result).getColor()).isEqualTo(Color.RED); } @Test public void testLoadColorDrawable_withNonUnitBitmapTransformation_returnsBitmapDrawable() { ColorDrawable colorDrawable = new ColorDrawable(Color.RED); requestManager .load(colorDrawable) .apply(new RequestOptions().override(100, 100).circleCrop()) .into(target); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); verify(target).onResourceReady(argumentCaptor.capture(), isA(Transition.class)); Object result = argumentCaptor.getValue(); assertThat(result).isInstanceOf(BitmapDrawable.class); Bitmap bitmap = ((BitmapDrawable) result).getBitmap(); assertThat(bitmap.getWidth()).isEqualTo(100); assertThat(bitmap.getHeight()).isEqualTo(100); } @Test public void testUriDefaultLoaderWithInputStream() { registerFailFactory(Uri.class, ParcelFileDescriptor.class); runTestUriDefaultLoader(); } @Test public void testUriDefaultLoaderWithFileDescriptor() { registerFailFactory(Uri.class, InputStream.class); runTestUriDefaultLoader(); } @Test public void testUriDefaultLoader() { runTestUriDefaultLoader(); } private void runTestUriDefaultLoader() { Uri uri = Uri.parse("content://test/something"); mockUri(uri); requestManager.load(uri).into(target); requestManager.load(uri).into(imageView); verify(target).onResourceReady(notNull(), isA(Transition.class)); verify(target).setRequest((Request) notNull()); assertNotNull(imageView.getDrawable()); } @Test public void testStringDefaultLoaderWithUrl() { runTestStringDefaultLoader("http://www.google.com"); } @Test public void testFileStringDefaultLoaderWithInputStream() { registerFailFactory(String.class, ParcelFileDescriptor.class); runTestFileStringDefaultLoader(); } @Test public void testFileStringDefaultLoaderWithFileDescriptor() { registerFailFactory(String.class, ParcelFileDescriptor.class); runTestFileStringDefaultLoader(); } @Test public void testFileStringDefaultLoader() { runTestFileStringDefaultLoader(); } private void runTestFileStringDefaultLoader() { String path = "/some/random/path"; mockUri(Uri.fromFile(new File(path))); runTestStringDefaultLoader(path); } @Test public void testUriStringDefaultLoaderWithInputStream() { registerFailFactory(String.class, ParcelFileDescriptor.class); runTestUriStringDefaultLoader(); } @Test public void testUriStringDefaultLoaderWithFileDescriptor() { registerFailFactory(String.class, InputStream.class); runTestUriStringDefaultLoader(); } @Test public void testUriStringDefaultLoader() { runTestUriStringDefaultLoader(); } private void runTestUriStringDefaultLoader() { String stringUri = "content://some/random/uri"; mockUri(Uri.parse(stringUri)); runTestStringDefaultLoader(stringUri); } private void runTestStringDefaultLoader(String string) { requestManager .load(string) .listener( new RequestListener() { @Override public boolean onLoadFailed( GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { throw new RuntimeException("Load failed"); } @Override public boolean onResourceReady( @NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { return false; } }) .into(target); requestManager.load(string).into(imageView); verify(target).onResourceReady(isA(BitmapDrawable.class), isA(Transition.class)); verify(target).setRequest((Request) notNull()); assertNotNull(imageView.getDrawable()); } @Test public void testIntegerDefaultLoaderWithInputStream() { registerFailFactory(Integer.class, ParcelFileDescriptor.class); runTestIntegerDefaultLoader(); } @Test public void testIntegerDefaultLoaderWithFileDescriptor() { registerFailFactory(Integer.class, InputStream.class); runTestIntegerDefaultLoader(); } @Test public void testIntegerDefaultLoader() { runTestIntegerDefaultLoader(); } private void runTestIntegerDefaultLoader() { int integer = android.R.drawable.star_on; mockUri("android.resource://" + "android" + "/drawable/star_on"); requestManager.load(integer).into(target); requestManager.load(integer).into(imageView); verify(target).onResourceReady(isA(BitmapDrawable.class), isA(Transition.class)); verify(target).setRequest((Request) notNull()); assertNotNull(imageView.getDrawable()); } @Test public void testByteArrayDefaultLoader() { byte[] bytes = new byte[10]; requestManager.load(bytes).into(target); requestManager.load(bytes).into(imageView); verify(target).onResourceReady(isA(BitmapDrawable.class), isA(Transition.class)); verify(target).setRequest((Request) notNull()); assertNotNull(imageView.getDrawable()); } @Test(expected = Exception.class) public void testUnregisteredModelThrowsException() { Float unregistered = 0.5f; requestManager.load(unregistered).into(target); } @Test @SuppressWarnings("unchecked") public void testNonDefaultModelWithRegisteredFactoryDoesNotThrow() { registerMockStreamModelLoader(Float.class); requestManager.load(0.5f).into(target); } @Test public void testReceivesGif() { String fakeUri = "content://fake"; InputStream testGifData = openGif(); mockUri(Uri.parse(fakeUri), testGifData); requestManager.asGif().load(fakeUri).into(target); verify(target).onResourceReady(isA(GifDrawable.class), isA(Transition.class)); } @Test public void testReceivesGifBytes() { String fakeUri = "content://fake"; InputStream testGifData = openGif(); mockUri(Uri.parse(fakeUri), testGifData); requestManager .as(byte[].class) .apply(decodeTypeOf(GifDrawable.class)) .load(fakeUri) .into(target); verify(target).onResourceReady(isA(byte[].class), isA(Transition.class)); } @Test public void testReceivesBitmapBytes() { String fakeUri = "content://fake"; mockUri(fakeUri); requestManager.as(byte[].class).apply(decodeTypeOf(Bitmap.class)).load(fakeUri).into(target); verify(target).onResourceReady(isA(byte[].class), isA(Transition.class)); } @Test public void testReceivesThumbnails() { String full = mockUri("content://full"); String thumb = mockUri("content://thumb"); requestManager.load(full).thumbnail(requestManager.load(thumb)).into(target); verify(target, times(2)).onResourceReady(isA(Drawable.class), isA(Transition.class)); } @Test public void testReceivesRecursiveThumbnails() { requestManager .load(mockUri("content://first")) .thumbnail( requestManager .load(mockUri("content://second")) .thumbnail( requestManager .load(mockUri("content://third")) .thumbnail(requestManager.load(mockUri("content://fourth"))))) .into(target); verify(target, times(4)).onResourceReady(isA(Drawable.class), isA(Transition.class)); } @Test public void testReceivesRecursiveThumbnailWithPercentage() { requestManager .load(mockUri("content://first")) .thumbnail(requestManager.load(mockUri("content://second")).thumbnail(0.5f)) .into(target); verify(target, times(3)).onResourceReady(isA(Drawable.class), isA(Transition.class)); } @Test public void testNullModelInGenericImageLoadDoesNotThrow() { requestManager.load(NULL).into(target); } @Test public void testNullModelInGenericVideoLoadDoesNotThrow() { requestManager.load(NULL).into(target); } @Test public void testNullModelInGenericLoadDoesNotThrow() { requestManager.load(NULL).into(target); } @Test public void testNullModelDoesNotThrow() { Drawable drawable = new ColorDrawable(Color.RED); requestManager.load(NULL).apply(errorOf(drawable)).into(target); verify(target).onLoadFailed(eq(drawable)); } @Test public void testNullModelPrefersErrorDrawable() { Drawable placeholder = new ColorDrawable(Color.GREEN); Drawable error = new ColorDrawable(Color.RED); requestManager.load(NULL).apply(placeholderOf(placeholder).error(error)).into(target); verify(target).onLoadFailed(eq(error)); } @Test public void testLoadBitmap_asBitmap() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); requestManager.asBitmap().load(bitmap).into(target); verify(target).onResourceReady(eq(bitmap), any(Transition.class)); } @Test public void testLoadBitmap_asDrawable() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); requestManager.load(bitmap).into(target); ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); verify(target).onResourceReady(captor.capture(), any(Transition.class)); BitmapDrawable drawable = (BitmapDrawable) captor.getValue(); assertThat(drawable.getBitmap()).isEqualTo(bitmap); } @Test public void testLoadDrawable() { Drawable drawable = new ColorDrawable(Color.RED); requestManager.load(drawable).into(target); ArgumentCaptor drawableCaptor = ArgumentCaptor.forClass(Drawable.class); verify(target).onResourceReady(drawableCaptor.capture(), any(Transition.class)); assertThat(((ColorDrawable) drawableCaptor.getValue()).getColor()).isEqualTo(Color.RED); } @Test public void testNullModelPrefersFallbackDrawable() { Drawable placeholder = new ColorDrawable(Color.GREEN); Drawable error = new ColorDrawable(Color.RED); Drawable fallback = new ColorDrawable(Color.BLUE); requestManager .load(NULL) .apply(placeholderOf(placeholder).error(error).fallback(fallback)) .into(target); verify(target).onLoadFailed(eq(fallback)); } @Test public void testNullModelResolvesToUsePlaceholder() { Drawable placeholder = new ColorDrawable(Color.GREEN); requestManager.load(NULL).apply(placeholderOf(placeholder)).into(target); verify(target).onLoadFailed(eq(placeholder)); } @Test public void testByteData() { byte[] data = new byte[] {1, 2, 3, 4, 5, 6}; requestManager.load(data).into(target); } @Test public void removeFromManagers_afterRequestManagerRemoved_clearsRequest() { target = requestManager .load(mockUri("content://uri")) .into( new CustomTarget() { @Override public void onResourceReady( @NonNull Drawable resource, @Nullable Transition transition) { // Do nothing. } @Override public void onLoadCleared(@Nullable Drawable placeholder) { // Do nothing, we don't retain a reference to our resource. } }); requestManager.onDestroy(); requestManager.clear(target); assertThat(target.getRequest()).isNull(); } @Test public void testClone() { Target firstTarget = mock(Target.class); doAnswer(new CallSizeReady(100, 100)).when(firstTarget).getSize(isA(SizeReadyCallback.class)); Target secondTarget = mock(Target.class); doAnswer(new CallSizeReady(100, 100)).when(secondTarget).getSize(isA(SizeReadyCallback.class)); RequestBuilder firstRequest = requestManager.load(mockUri("content://first")); firstRequest.into(firstTarget); firstRequest.clone().apply(placeholderOf(new ColorDrawable(Color.RED))).into(secondTarget); verify(firstTarget).onResourceReady(isA(Drawable.class), isA(Transition.class)); verify(secondTarget) .onResourceReady(ArgumentMatchers.notNull(), isA(Transition.class)); } @SuppressWarnings("unchecked") private void registerFailFactory(Class failModel, Class failResource) { DataFetcher failFetcher = mock(DataFetcher.class); doAnswer(new Util.CallDataReady<>(null)) .when(failFetcher) .loadData(isA(Priority.class), isA(DataFetcher.DataCallback.class)); when(failFetcher.getDataClass()).thenReturn(failResource); ModelLoader failLoader = mock(ModelLoader.class); when(failLoader.buildLoadData(isA(failModel), anyInt(), anyInt(), isA(Options.class))) .thenReturn(new ModelLoader.LoadData<>(mock(Key.class), failFetcher)); when(failLoader.handles(isA(failModel))).thenReturn(true); ModelLoaderFactory failFactory = mock(ModelLoaderFactory.class); when(failFactory.build(isA(MultiModelLoaderFactory.class))).thenReturn(failLoader); Glide.get(context).getRegistry().prepend(failModel, failResource, failFactory); } private String mockUri(String uriString) { return mockUri(Uri.parse(uriString), null); } private void mockUri(Uri uri) { mockUri(uri, null); } private String mockUri(Uri uri, InputStream is) { if (is == null) { is = new ByteArrayInputStream(new byte[0]); } ContentResolver contentResolver = context.getContentResolver(); ShadowFileDescriptorContentResolver shadowContentResolver = Shadow.extract(contentResolver); shadowContentResolver.registerInputStream(uri, is); AssetFileDescriptor assetFileDescriptor = mock(AssetFileDescriptor.class); ParcelFileDescriptor parcelFileDescriptor = mock(ParcelFileDescriptor.class); when(assetFileDescriptor.getParcelFileDescriptor()).thenReturn(parcelFileDescriptor); shadowContentResolver.registerAssetFileDescriptor(uri, assetFileDescriptor); return uri.toString(); } @SuppressWarnings("unchecked") private void registerMockStreamModelLoader(final Class modelClass) { ModelLoader modelLoader = mockStreamModelLoader(modelClass); ModelLoaderFactory modelLoaderFactory = mock(ModelLoaderFactory.class); when(modelLoaderFactory.build(isA(MultiModelLoaderFactory.class))).thenReturn(modelLoader); Glide.get(context).getRegistry().prepend(modelClass, InputStream.class, modelLoaderFactory); } @SuppressWarnings("unchecked") private ModelLoader mockStreamModelLoader(final Class modelClass) { ModelLoader modelLoader = mock(ModelLoader.class); DataFetcher fetcher = mock(DataFetcher.class); try { doAnswer(new Util.CallDataReady<>(new ByteArrayInputStream(new byte[0]))) .when(fetcher) .loadData(isA(Priority.class), isA(DataFetcher.DataCallback.class)); } catch (Exception e) { // Do nothing. } when(fetcher.getDataClass()).thenReturn(InputStream.class); when(modelLoader.buildLoadData(isA(modelClass), anyInt(), anyInt(), isA(Options.class))) .thenReturn(new ModelLoader.LoadData<>(mock(Key.class), fetcher)); when(modelLoader.handles(isA(modelClass))).thenReturn(true); return modelLoader; } private InputStream openGif() { return TestResourceUtil.openResource(getClass(), "test.gif"); } private static class CallSizeReady implements Answer { private final int width; private final int height; CallSizeReady() { this(100, 100); } CallSizeReady(int width, int height) { this.width = width; this.height = height; } @Override public Void answer(InvocationOnMock invocation) throws Throwable { SizeReadyCallback cb = (SizeReadyCallback) invocation.getArguments()[0]; cb.onSizeReady(width, height); return null; } } private static void registerMockModelLoader( Class modelClass, Class dataClass, Y loadedData, Registry registry) { DataFetcher mockStreamFetcher = mock(DataFetcher.class); when(mockStreamFetcher.getDataClass()).thenReturn(dataClass); try { doAnswer(new Util.CallDataReady<>(loadedData)) .when(mockStreamFetcher) .loadData(isA(Priority.class), isA(DataFetcher.DataCallback.class)); } catch (Exception e) { throw new RuntimeException(e); } ModelLoader mockUrlLoader = mock(ModelLoader.class); when(mockUrlLoader.buildLoadData(isA(modelClass), anyInt(), anyInt(), isA(Options.class))) .thenReturn(new ModelLoader.LoadData<>(mock(Key.class), mockStreamFetcher)); when(mockUrlLoader.handles(isA(modelClass))).thenReturn(true); ModelLoaderFactory mockUrlLoaderFactory = mock(ModelLoaderFactory.class); when(mockUrlLoaderFactory.build(isA(MultiModelLoaderFactory.class))).thenReturn(mockUrlLoader); registry.replace(modelClass, dataClass, mockUrlLoaderFactory); } // TODO: Extending ShadowContentResolver results in exceptions because of some state issues // where we seem to get one content resolver shadow in one part of the test and a different one in // a different part of the test. Each one ends up with different registered uris, which causes // tests to fail. We shouldn't need to do this, but using static maps seems to fix the issue. @Implements(value = ContentResolver.class) @SuppressWarnings("unused") public static class ShadowFileDescriptorContentResolver { private static final Map URI_TO_FILE_DESCRIPTOR = new HashMap<>(); private static final Map URI_TO_INPUT_STREAMS = new HashMap<>(); @Resetter public static void reset() { URI_TO_INPUT_STREAMS.clear(); URI_TO_FILE_DESCRIPTOR.clear(); } void registerInputStream(Uri uri, InputStream inputStream) { URI_TO_INPUT_STREAMS.put(uri, inputStream); } void registerAssetFileDescriptor(Uri uri, AssetFileDescriptor assetFileDescriptor) { URI_TO_FILE_DESCRIPTOR.put(uri, assetFileDescriptor); } @Implementation public InputStream openInputStream(Uri uri) { if (!URI_TO_INPUT_STREAMS.containsKey(uri)) { throw new IllegalArgumentException( "You must first register an InputStream for uri: " + uri); } return URI_TO_INPUT_STREAMS.get(uri); } @Implementation public AssetFileDescriptor openAssetFileDescriptor(Uri uri, String type) { if (!URI_TO_FILE_DESCRIPTOR.containsKey(uri)) { throw new IllegalArgumentException( "You must first register an AssetFileDescriptor for " + "uri: " + uri); } return URI_TO_FILE_DESCRIPTOR.get(uri); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/InitializeGlideTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.tests.TearDownGlide; import java.util.Set; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; // This test is about edge cases that might otherwise make debugging more challenging. @RunWith(AndroidJUnit4.class) public class InitializeGlideTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final Context context = ApplicationProvider.getApplicationContext(); private static final class TestException extends RuntimeException { private static final long serialVersionUID = 2515021766931124927L; } @Test public void initialize_whenInternalMethodThrows_throwsException() { assertThrows( TestException.class, new ThrowingRunnable() { @Override public void run() { synchronized (Glide.class) { Glide.checkAndInitializeGlide( context, new GeneratedAppGlideModule() { @NonNull @Override Set> getExcludedModuleClasses() { throw new TestException(); } }); } } }); } @Test public void initialize_whenInternalMethodThrows_andCalledTwice_throwsException() { GeneratedAppGlideModule throwingGeneratedAppGlideModule = new GeneratedAppGlideModule() { @NonNull @Override Set> getExcludedModuleClasses() { throw new TestException(); } }; ThrowingRunnable initializeGlide = new ThrowingRunnable() { @Override public void run() throws Throwable { synchronized (Glide.class) { Glide.checkAndInitializeGlide(context, throwingGeneratedAppGlideModule); } } }; assertThrows(TestException.class, initializeGlide); // Make sure the second exception isn't hidden by some Glide initialization related exception. assertThrows(TestException.class, initializeGlide); } @Test public void isInitialized_whenNotInitialized_returnsFalse() { assertThat(Glide.isInitialized()).isFalse(); } @Test public void isInitialized_whenInitialized_returnsTrue() { Glide.get(context); assertThat(Glide.isInitialized()).isTrue(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/ListPreloaderTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.tests.Util.cast; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.verification.VerificationMode; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK) public class ListPreloaderTest { @Mock private RequestBuilder request; @Mock private RequestManager requestManager; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void testGetItemsIsCalledIncreasing() { final AtomicBoolean called = new AtomicBoolean(false); final AtomicInteger calledCount = new AtomicInteger(); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { @NonNull @Override public List getPreloadItems(int position) { called.set(true); final int count = calledCount.getAndIncrement(); assertEquals(11 + count, position); return super.getPreloadItems(position); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); preloader.onScroll(null, 1, 10, 30); assertEquals(10, calledCount.get()); } @Test public void testGetItemsIsCalledInOrderIncreasing() { final int toPreload = 10; final List objects = new ArrayList<>(); for (int i = 0; i < toPreload; i++) { objects.add(i); } ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { private int expectedPosition; @Override public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) { return new int[] {10, 10}; } @NonNull @Override public List getPreloadItems(int position) { return objects.subList(position - 11, position + 1 - 11); } @Nullable @Override @SuppressWarnings("unchecked") public RequestBuilder getPreloadRequestBuilder(@NonNull Object item) { assertEquals(objects.get(expectedPosition), item); expectedPosition++; return mock(RequestBuilder.class); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, toPreload); preloader.onScroll(null, 1, 10, 20); } @Test public void testGetItemsIsCalledDecreasing() { final AtomicBoolean called = new AtomicBoolean(false); final AtomicInteger calledCount = new AtomicInteger(); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { @NonNull @Override public List getPreloadItems(int position) { // Ignore the preload caused from us starting at the end if (position >= 40) { return Collections.emptyList(); } final int count = calledCount.getAndIncrement(); called.set(true); assertEquals(28 - count, position); return super.getPreloadItems(position); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); preloader.onScroll(null, 30, 10, 40); preloader.onScroll(null, 29, 10, 40); assertTrue(called.get()); } @Test public void testGetItemsIsCalledInOrderDecreasing() { final int toPreload = 10; final List objects = new ArrayList<>(); for (int i = 0; i < toPreload; i++) { objects.add(new Object()); } ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { private int expectedPosition = toPreload - 1; @Override public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) { return new int[] {10, 10}; } @NonNull @Override public List getPreloadItems(int position) { if (position == 40) { return Collections.emptyList(); } return objects.subList(position, position + 1); } @Nullable @Override @SuppressWarnings("unchecked") public RequestBuilder getPreloadRequestBuilder(@NonNull Object item) { assertEquals(objects.get(expectedPosition), item); expectedPosition--; return mock(RequestBuilder.class); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, toPreload); preloader.onScroll(null, 30, 10, 10); preloader.onScroll(null, 29, 10, 10); } @Test public void testGetItemsIsNeverCalledWithEndGreaterThanTotalItems() { final AtomicBoolean called = new AtomicBoolean(false); final AtomicInteger calledCount = new AtomicInteger(); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { @NonNull @Override public List getPreloadItems(int position) { called.set(true); final int count = calledCount.getAndIncrement(); assertEquals(26 + count, position); return super.getPreloadItems(position); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); preloader.onScroll(null, 16, 10, 30); assertTrue(called.get()); } @Test public void testGetItemsIsNeverCalledWithStartLessThanZero() { final AtomicBoolean called = new AtomicBoolean(false); final AtomicInteger calledCount = new AtomicInteger(); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { @NonNull @Override public List getPreloadItems(int position) { if (position >= 17) { return Collections.emptyList(); } called.set(true); final int count = calledCount.getAndIncrement(); assertEquals(5 - count, position); return super.getPreloadItems(position); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); preloader.onScroll(null, 7, 10, 30); preloader.onScroll(null, 6, 10, 30); assertTrue(called.get()); } @Test public void testDontPreloadItemsRepeatedlyWhileIncreasing() { final AtomicInteger called = new AtomicInteger(); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { @NonNull @Override public List getPreloadItems(int position) { final int current = called.getAndIncrement(); assertEquals(11 + current, position); return super.getPreloadItems(position); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); preloader.onScroll(null, 1, 10, 30); preloader.onScroll(null, 4, 10, 30); assertEquals(13, called.get()); } @Test public void testDontPreloadItemsRepeatedlyWhileDecreasing() { final AtomicInteger called = new AtomicInteger(); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { @NonNull @Override public List getPreloadItems(int position) { if (position >= 20) { return Collections.emptyList(); } final int current = called.getAndIncrement(); assertEquals(19 - current, position); return super.getPreloadItems(position); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); preloader.onScroll(null, 21, 10, 30); preloader.onScroll(null, 20, 10, 30); preloader.onScroll(null, 17, 10, 30); assertEquals(13, called.get()); } @Test public void testMultipleItemsForPositionIncreasing() { final List objects = new ArrayList<>(); objects.add(new Object()); objects.add(new Object()); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { private int expectedPosition = (1 + 10) * 2; @NonNull @Override public List getPreloadItems(int position) { return objects; } @Override public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) { assertEquals(expectedPosition / 2, adapterPosition); assertEquals(expectedPosition % 2, itemPosition); expectedPosition++; return itemPosition == 0 ? new int[] {10, 11} : new int[] {20, 21}; } @Nullable @Override public RequestBuilder getPreloadRequestBuilder(@NonNull Object item) { return request; } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); Iterable expected = Arrays.asList(10, 11, 20, 21, 10, 11, 20, 21); preloader.onScroll(null, 1, 10, 1 + 10 + 2); List allValues = getTargetsSizes(request, times(4)); assertEquals(expected, allValues); } @Test public void testMultipleItemsForPositionDecreasing() { final List objects = new ArrayList<>(); objects.add(new Object()); objects.add(new Object()); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { private int expectedPosition = objects.size() * 2 - 1; @NonNull @Override public List getPreloadItems(int position) { return objects; } @Override public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) { assertEquals(expectedPosition / 2, adapterPosition); assertEquals(expectedPosition % 2, itemPosition); expectedPosition--; return itemPosition == 0 ? new int[] {10, 11} : new int[] {20, 21}; } @Nullable @Override public RequestBuilder getPreloadRequestBuilder(@NonNull Object item) { return request; } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); Iterable expected = Arrays.asList(20, 21, 10, 11, 20, 21, 10, 11); preloader.onScroll(null, 3, 2, 3 + 2); preloader.onScroll(null, 2, 2, 3 + 2); List allValues = getTargetsSizes(request, times(4)); assertEquals(expected, allValues); } private List getTargetsSizes( RequestBuilder requestBuilder, VerificationMode mode) { ArgumentCaptor integerArgumentCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor> targetArgumentCaptor = cast(ArgumentCaptor.forClass(Target.class)); SizeReadyCallback cb = mock(SizeReadyCallback.class); verify(requestBuilder, mode).into(targetArgumentCaptor.capture()); for (Target target : targetArgumentCaptor.getAllValues()) { target.getSize(cb); } verify(cb, mode).onSizeReady(integerArgumentCaptor.capture(), integerArgumentCaptor.capture()); return integerArgumentCaptor.getAllValues(); } // It's safe to ignore the return value of containsAllIn. @SuppressWarnings("ResultOfMethodCallIgnored") @Test public void testItemsArePreloadedWithGlide() { final List objects = new ArrayList<>(); objects.add(new Object()); objects.add(new Object()); final HashSet loadedObjects = new HashSet<>(); ListPreloaderAdapter preloaderAdapter = new ListPreloaderAdapter() { @NonNull @Override public List getPreloadItems(int position) { return objects.subList(position - 11, position - 10); } @Nullable @Override public RequestBuilder getPreloadRequestBuilder(@NonNull Object item) { loadedObjects.add(item); return super.getPreloadRequestBuilder(item); } }; ListPreloader preloader = new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10); preloader.onScroll(null, 1, 10, 13); assertThat(loadedObjects).containsAtLeastElementsIn(objects); } private static class ListPreloaderAdapter implements ListPreloader.PreloadModelProvider, ListPreloader.PreloadSizeProvider { public ListPreloaderAdapter() {} @NonNull @Override public List getPreloadItems(int position) { ArrayList result = new ArrayList<>(1); Collections.fill(result, new Object()); return result; } @Nullable @Override @SuppressWarnings("unchecked") public RequestBuilder getPreloadRequestBuilder(@NonNull Object item) { return mock(RequestBuilder.class); } @Nullable @Override public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) { return new int[] {100, 100}; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/RegistryFactoryTest.java ================================================ package com.bumptech.glide; import static org.junit.Assert.assertThrows; import android.content.Context; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.tests.TearDownGlide; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import com.google.common.collect.ImmutableList; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class RegistryFactoryTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final Context context = ApplicationProvider.getApplicationContext(); private static final class TestException extends RuntimeException { private static final long serialVersionUID = 2334956185897161236L; } @Test public void create_whenCalledTwiceWithThrowingModule_throwsOriginalException() { AppGlideModule throwingAppGlideModule = new AppGlideModule() { @Override public void registerComponents( @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { throw new TestException(); } }; Glide glide = Glide.get(context); GlideSupplier registrySupplier = RegistryFactory.lazilyCreateAndInitializeRegistry( glide, /* manifestModules= */ ImmutableList.of(), throwingAppGlideModule); assertThrows( TestException.class, new ThrowingRunnable() { @Override public void run() { registrySupplier.get(); } }); assertThrows( TestException.class, new ThrowingRunnable() { @Override public void run() { registrySupplier.get(); } }); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/RegistryTest.java ================================================ package com.bumptech.glide; import static com.google.common.truth.Truth.assertThat; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class RegistryTest { @Mock private ModelLoaderFactory modelLoaderFactory; @Mock private ResourceDecoder resourceOneDecoder; @Mock private ResourceDecoder resourceTwoDecoder; @Mock private ResourceTranscoder resourceOneTranscodeOneTranscoder; private Registry registry; @Before public void setUp() { MockitoAnnotations.initMocks(this); registry = new Registry(); } @Test public void getRegisteredResourceClasses_withNoResources_isEmpty() { assertThat(getRegisteredResourceClasses()).isEmpty(); } @Test public void getRegisteredResourceClasses_withOneDataClass_noResourceClasses_isEmpty() { registry.append(Model.class, Data.class, modelLoaderFactory); assertThat(getRegisteredResourceClasses()).isEmpty(); } @Test public void getRegisteredResourceClasses_withOneDataAndResourceClass_noTranscodeClass_isEmpty() { registry.append(Model.class, Data.class, modelLoaderFactory); registry.append(Data.class, ResourceOne.class, resourceOneDecoder); assertThat(getRegisteredResourceClasses()).isEmpty(); } @Test public void getRegisteredResourceClasses_withOneDataAndResourceAndTranscodeClass_isNotEmpty() { registry.append(Model.class, Data.class, modelLoaderFactory); registry.append(Data.class, ResourceOne.class, resourceOneDecoder); registry.register(ResourceOne.class, TranscodeOne.class, resourceOneTranscodeOneTranscoder); assertThat(getRegisteredResourceClasses()).containsExactly(ResourceOne.class); } @Test public void getRegisteredResourceClasses_withMissingTranscodeForOneOfTwoResources_isNotEmpty() { // The loop allows us to make sure that the order in which we call getRegisteredResourceClasses // doesn't affect the output. for (int i = 0; i < 2; i++) { Registry registry = new Registry(); registry.append(Model.class, Data.class, modelLoaderFactory); registry.append(Data.class, ResourceOne.class, resourceOneDecoder); registry.append(Data.class, ResourceTwo.class, resourceTwoDecoder); registry.register(ResourceOne.class, TranscodeOne.class, resourceOneTranscodeOneTranscoder); List> resourceOneClasses; List> resourceTwoClasses; if (i == 0) { resourceOneClasses = registry.getRegisteredResourceClasses( Model.class, ResourceOne.class, TranscodeOne.class); resourceTwoClasses = registry.getRegisteredResourceClasses( Model.class, ResourceTwo.class, TranscodeOne.class); } else { resourceTwoClasses = registry.getRegisteredResourceClasses( Model.class, ResourceTwo.class, TranscodeOne.class); resourceOneClasses = registry.getRegisteredResourceClasses( Model.class, ResourceOne.class, TranscodeOne.class); } // ResourceOne has a corresponding transcode class, so we should return it. assertThat(resourceOneClasses).containsExactly(ResourceOne.class); // ResourceTwo has no matching transcode class, so we shouldn't return it. assertThat(resourceTwoClasses).isEmpty(); } } @Test public void getRegisteredResourceClasses_withOneOfTwoMissingTranscoders_isNotEmpty() { // The loop allows us to make sure that the order in which we call getRegisteredResourceClasses // doesn't affect the output. for (int i = 0; i < 2; i++) { Registry registry = new Registry(); registry.append(Model.class, Data.class, modelLoaderFactory); registry.append(Data.class, ResourceOne.class, resourceOneDecoder); registry.register(ResourceOne.class, TranscodeOne.class, resourceOneTranscodeOneTranscoder); List> transcodeOneClasses; List> transcodeTwoClasses; if (i == 0) { transcodeOneClasses = registry.getRegisteredResourceClasses( Model.class, ResourceOne.class, TranscodeOne.class); transcodeTwoClasses = registry.getRegisteredResourceClasses( Model.class, ResourceOne.class, TranscodeTwo.class); } else { transcodeTwoClasses = registry.getRegisteredResourceClasses( Model.class, ResourceOne.class, TranscodeTwo.class); transcodeOneClasses = registry.getRegisteredResourceClasses( Model.class, ResourceOne.class, TranscodeOne.class); } // TranscodeOne has a corresponding ResourceTranscoder, so we expect to see the resource // class. assertThat(transcodeOneClasses).containsExactly(ResourceOne.class); // TranscodeTwo has no corresponding ResourceTranscoder class, so we shouldn't return the // resource class. assertThat(transcodeTwoClasses).isEmpty(); } } private List> getRegisteredResourceClasses() { return registry.getRegisteredResourceClasses( Model.class, ResourceOne.class, TranscodeOne.class); } private static final class Model { // Empty class to represent model classes for readability. } private static final class Data { // Empty class to represent data classes for readability. } private static final class ResourceOne { // Empty class to represent resource classes for readability. } private static final class ResourceTwo { // Empty class to represent another resource class for readability. } private static final class TranscodeOne { // Empty class to represent transcode classes for readability. } private static final class TranscodeTwo { // Empty class to represent transcode classes for readability. } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.tests.BackgroundUtil.testInBackground; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.net.Uri; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.SimpleResource; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.SingleRequest; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.target.ViewTarget; import com.bumptech.glide.tests.BackgroundUtil.BackgroundTester; import com.bumptech.glide.tests.TearDownGlide; import com.google.common.testing.EqualsTester; import java.util.concurrent.Executors; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @SuppressWarnings("unchecked") @RunWith(RobolectricTestRunner.class) @Config(sdk = com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK) public class RequestBuilderTest { @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private RequestListener listener1; @Mock private RequestListener listener2; @Mock private Target target; @Mock private GlideContext glideContext; @Mock private RequestManager requestManager; @Captor private ArgumentCaptor> requestCaptor; private Glide glide; private Application context; @Before public void setUp() { MockitoAnnotations.initMocks(this); glide = Glide.get(ApplicationProvider.getApplicationContext()); context = ApplicationProvider.getApplicationContext(); } @Test(expected = NullPointerException.class) public void testThrowsIfContextIsNull() { new RequestBuilder<>(null /*context*/, requestManager, Object.class, context); } @Test(expected = NullPointerException.class) public void testThrowsWhenTransitionsOptionsIsNull() { //noinspection ConstantConditions testing if @NonNull is enforced getNullModelRequest().transition(null); } @Test public void testDoesNotThrowWithNullModelWhenRequestIsBuilt() { getNullModelRequest().into(target); } @Test public void testDoesNotThrowWithNullModelWhenRequestIsBuiltFront() { getNullModelRequest().experimentalIntoFront(target); } @Test public void testAddsNewRequestToRequestTracker() { getNullModelRequest().into(target); verify(requestManager).track(eq(target), isA(Request.class)); } @Test public void testAddsNewRequestToRequestTrackerWithCustomExecutor() { getNullModelRequest() .into(target, /* targetListener= */ null, Executors.newSingleThreadExecutor()); verify(requestManager).track(eq(target), isA(Request.class)); } @Test public void testAddsNewRequestToRequestTrackerFront() { getNullModelRequest().experimentalIntoFront(target); verify(requestManager).track(eq(target), isA(Request.class)); } @Test public void testRemovesPreviousRequestFromRequestTracker() { Request previous = mock(Request.class); when(target.getRequest()).thenReturn(previous); getNullModelRequest().into(target); verify(requestManager).clear(eq(target)); } @Test public void testRemovesPreviousRequestFromRequestTrackerFront() { Request previous = mock(Request.class); when(target.getRequest()).thenReturn(previous); getNullModelRequest().experimentalIntoFront(target); verify(requestManager).clear(eq(target)); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullTarget() { //noinspection ConstantConditions testing if @NonNull is enforced getNullModelRequest().into((Target) null); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullTargetFront() { //noinspection ConstantConditions testing if @NonNull is enforced getNullModelRequest().experimentalIntoFront((Target) null); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullView() { getNullModelRequest().into((ImageView) null); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullViewFront() { getNullModelRequest().experimentalIntoFront((ImageView) null); } @Test(expected = RuntimeException.class) public void testThrowsIfIntoViewCalledOnBackgroundThread() throws InterruptedException { final ImageView imageView = new ImageView(ApplicationProvider.getApplicationContext()); testInBackground( new BackgroundTester() { @Override public void runTest() { getNullModelRequest().into(imageView); } }); } @Test(expected = RuntimeException.class) public void testThrowsIfIntoViewCalledOnBackgroundThreadFront() throws InterruptedException { final ImageView imageView = new ImageView(ApplicationProvider.getApplicationContext()); testInBackground( new BackgroundTester() { @Override public void runTest() { getNullModelRequest().experimentalIntoFront(imageView); } }); } @Test public void doesNotThrowIfIntoTargetCalledOnBackgroundThread() throws InterruptedException { final Target target = mock(Target.class); testInBackground( new BackgroundTester() { @Override public void runTest() { getNullModelRequest().into(target); } }); } @Test public void doesNotThrowIfIntoTargetCalledOnBackgroundThreadFront() throws InterruptedException { final Target target = mock(Target.class); testInBackground( new BackgroundTester() { @Override public void runTest() { getNullModelRequest().experimentalIntoFront(target); } }); } @Test public void doesNotThrowIfIntoTargetWithCustomExecutorCalledOnBackgroundThread() throws InterruptedException { final Target target = mock(Target.class); testInBackground( new BackgroundTester() { @Override public void runTest() { getNullModelRequest() .into(target, /* targetListener= */ null, Executors.newSingleThreadExecutor()); } }); } @Test public void testMultipleRequestListeners() { getNullModelRequest().addListener(listener1).addListener(listener2).into(target); verify(requestManager).track(any(Target.class), requestCaptor.capture()); requestCaptor .getValue() .onResourceReady( new SimpleResource<>(new Object()), DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); verify(listener2) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); } @Test public void testMultipleRequestListenersFront() { getNullModelRequest() .addListener(listener1) .addListener(listener2) .experimentalIntoFront(target); verify(requestManager).track(any(Target.class), requestCaptor.capture()); requestCaptor .getValue() .onResourceReady( new SimpleResource<>(new Object()), DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); verify(listener2) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); } @Test public void testListenerApiOverridesListeners() { getNullModelRequest().addListener(listener1).listener(listener2).into(target); verify(requestManager).track(any(Target.class), requestCaptor.capture()); requestCaptor .getValue() .onResourceReady( new SimpleResource<>(new Object()), DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); // The #listener API removes any previous listeners, so the first listener should not be called. verify(listener1, never()) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); verify(listener2) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); } @Test public void testListenerApiOverridesListenersFront() { getNullModelRequest().addListener(listener1).listener(listener2).experimentalIntoFront(target); verify(requestManager).track(any(Target.class), requestCaptor.capture()); requestCaptor .getValue() .onResourceReady( new SimpleResource<>(new Object()), DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); // The #listener API removes any previous listeners, so the first listener should not be called. verify(listener1, never()) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); verify(listener2) .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); } @Test public void testEquals() { Object firstModel = new Object(); Object secondModel = new Object(); RequestListener firstListener = new RequestListener<>() { @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( @NonNull Object resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { return false; } }; RequestListener secondListener = new RequestListener<>() { @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( @NonNull Object resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { return false; } }; new EqualsTester() .addEqualityGroup(new Object()) .addEqualityGroup(newRequestBuilder(Object.class), newRequestBuilder(Object.class)) .addEqualityGroup( newRequestBuilder(Object.class).load((Object) null), newRequestBuilder(Object.class).load((Object) null), newRequestBuilder(Object.class).load((Uri) null)) .addEqualityGroup( newRequestBuilder(Object.class).load(firstModel), newRequestBuilder(Object.class).load(firstModel)) .addEqualityGroup( newRequestBuilder(Object.class).load(secondModel), newRequestBuilder(Object.class).load(secondModel)) .addEqualityGroup( newRequestBuilder(Object.class).load(Uri.EMPTY), newRequestBuilder(Object.class).load(Uri.EMPTY)) .addEqualityGroup( newRequestBuilder(Uri.class).load(Uri.EMPTY), newRequestBuilder(Uri.class).load(Uri.EMPTY)) .addEqualityGroup( newRequestBuilder(Object.class).centerCrop(), newRequestBuilder(Object.class).centerCrop()) .addEqualityGroup( newRequestBuilder(Object.class).addListener(firstListener), newRequestBuilder(Object.class).addListener(firstListener)) .addEqualityGroup( newRequestBuilder(Object.class).addListener(secondListener), newRequestBuilder(Object.class).addListener(secondListener)) .addEqualityGroup( newRequestBuilder(Object.class).error(newRequestBuilder(Object.class)), newRequestBuilder(Object.class).error(newRequestBuilder(Object.class))) .addEqualityGroup( newRequestBuilder(Object.class).error(firstModel), newRequestBuilder(Object.class).error(firstModel), newRequestBuilder(Object.class).error(newRequestBuilder(Object.class).load(firstModel))) .addEqualityGroup( newRequestBuilder(Object.class).error(secondModel), newRequestBuilder(Object.class).error(secondModel), newRequestBuilder(Object.class) .error(newRequestBuilder(Object.class).load(secondModel))) .addEqualityGroup( newRequestBuilder(Object.class) .error(newRequestBuilder(Object.class).load(firstModel).centerCrop()), newRequestBuilder(Object.class) .error(newRequestBuilder(Object.class).load(firstModel).centerCrop())) .addEqualityGroup( newRequestBuilder(Object.class) .thumbnail(newRequestBuilder(Object.class).load(firstModel)), newRequestBuilder(Object.class) .thumbnail(newRequestBuilder(Object.class).load(firstModel))) .addEqualityGroup( newRequestBuilder(Object.class) .thumbnail(newRequestBuilder(Object.class).load(secondModel)), newRequestBuilder(Object.class) .thumbnail(newRequestBuilder(Object.class).load(secondModel))) .addEqualityGroup( newRequestBuilder(Object.class) .transition(new GenericTransitionOptions<>().dontTransition()), newRequestBuilder(Object.class) .transition(new GenericTransitionOptions<>().dontTransition())) .testEquals(); } private RequestBuilder getNullModelRequest() { return newRequestBuilder(Object.class).load((Object) null); } private RequestBuilder newRequestBuilder(Class modelClass) { when(glideContext.buildImageViewTarget(isA(ImageView.class), isA(Class.class))) .thenReturn(mock(ViewTarget.class)); when(glideContext.getDefaultRequestOptions()).thenReturn(new RequestOptions()); when(requestManager.getDefaultRequestOptions()).thenReturn(new RequestOptions()); when(requestManager.getDefaultTransitionOptions(any(Class.class))) .thenReturn(new GenericTransitionOptions<>()); return new RequestBuilder<>(glide, requestManager, modelClass, context); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/RequestManagerTest.java ================================================ package com.bumptech.glide; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.BackgroundUtil.testInBackground; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.manager.ConnectivityMonitor; import com.bumptech.glide.manager.ConnectivityMonitor.ConnectivityListener; import com.bumptech.glide.manager.ConnectivityMonitorFactory; import com.bumptech.glide.manager.Lifecycle; import com.bumptech.glide.manager.RequestManagerTreeNode; import com.bumptech.glide.manager.RequestTracker; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.tests.BackgroundUtil; import com.bumptech.glide.tests.TearDownGlide; import java.io.File; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class RequestManagerTest { @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private Lifecycle lifecycle = mock(Lifecycle.class); @Mock private RequestManagerTreeNode treeNode = mock(RequestManagerTreeNode.class); private RequestManager manager; private ConnectivityMonitor connectivityMonitor; private RequestTracker requestTracker; private ConnectivityListener connectivityListener; private Application context; private CustomTarget target; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); connectivityMonitor = mock(ConnectivityMonitor.class); ConnectivityMonitorFactory factory = mock(ConnectivityMonitorFactory.class); when(factory.build(isA(Context.class), isA(ConnectivityMonitor.ConnectivityListener.class))) .thenAnswer( new Answer() { @Override public ConnectivityMonitor answer(InvocationOnMock invocation) { connectivityListener = (ConnectivityListener) invocation.getArguments()[1]; return connectivityMonitor; } }); target = new CustomTarget() { @Override public void onResourceReady( @NonNull Drawable resource, @Nullable Transition transition) { // Empty. } @Override public void onLoadCleared(@Nullable Drawable placeholder) {} }; requestTracker = mock(RequestTracker.class); manager = new RequestManager( Glide.get(ApplicationProvider.getApplicationContext()), lifecycle, treeNode, requestTracker, factory, context); } @Test public void testPauseRequestsPausesRequests() { manager.pauseRequests(); verify(requestTracker).pauseRequests(); } @Test public void testResumeRequestsResumesRequests() { manager.resumeRequests(); verify(requestTracker).resumeRequests(); } @Test public void testPausesRequestsOnStop() { manager.onStart(); manager.onStop(); verify(requestTracker).pauseRequests(); } @Test public void testResumesRequestsOnStart() { manager.onStart(); verify(requestTracker).resumeRequests(); } @Test public void testClearsRequestsOnDestroy() { manager.onDestroy(); verify(requestTracker).clearRequests(); } @Test public void testAddsConnectivityMonitorToLifecycleWhenConstructed() { verify(lifecycle).addListener(eq(connectivityMonitor)); } @Test public void testAddsSelfToLifecycleWhenConstructed() { verify(lifecycle).addListener(eq(manager)); } @Test public void testRestartsRequestOnConnected() { connectivityListener.onConnectivityChanged(true); verify(requestTracker).restartRequests(); } @Test public void testDoesNotRestartRequestsOnDisconnected() { connectivityListener.onConnectivityChanged(false); verify(requestTracker, never()).restartRequests(); } @Test public void resumeRequests_whenCalledOnBackgroundThread_doesNotThrow() throws InterruptedException { testInBackground( new BackgroundUtil.BackgroundTester() { @Override public void runTest() { manager.resumeRequests(); } }); } @Test public void pauseRequests_whenCalledOnBackgroundThread_doesNotThrow() throws InterruptedException { testInBackground( new BackgroundUtil.BackgroundTester() { @Override public void runTest() { manager.pauseRequests(); } }); } @Test public void testDelegatesIsPausedToRequestTracker() { when(requestTracker.isPaused()).thenReturn(true); assertTrue(manager.isPaused()); when(requestTracker.isPaused()).thenReturn(false); assertFalse(manager.isPaused()); } @Test public void clear_withRequestStartedInSiblingManager_doesNotThrow() { final RequestManager child1 = new RequestManager( Glide.get(context), lifecycle, new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return Collections.emptySet(); } }, context); final RequestManager child2 = new RequestManager( Glide.get(context), lifecycle, new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return Collections.emptySet(); } }, context); new RequestManager( Glide.get(context), lifecycle, new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return new HashSet<>(java.util.Arrays.asList(child1, child2)); } }, context); File file = new File("fake"); child1.load(file).into(target); child2.clear(target); } @Test public void clear_withRequestStartedInChildManager_doesNotThrow() { final RequestManager child = new RequestManager( Glide.get(context), lifecycle, new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return Collections.emptySet(); } }, context); RequestManager parent = new RequestManager( Glide.get(context), lifecycle, new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return Collections.singleton(child); } }, context); File file = new File("fake"); child.load(file).into(target); parent.clear(target); } @Test public void clear_withRequestStartedInParentManager_doesNotThrow() { final RequestManager child = new RequestManager( Glide.get(context), lifecycle, new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return Collections.emptySet(); } }, context); RequestManager parent = new RequestManager( Glide.get(context), lifecycle, new RequestManagerTreeNode() { @NonNull @Override public Set getDescendants() { return Collections.singleton(child); } }, context); File file = new File("fake"); parent.load(file).into(target); child.clear(target); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/ImageHeaderParserUtilsTest.java ================================================ package com.bumptech.glide.load; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; import android.content.Context; import android.os.ParcelFileDescriptor; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.util.ByteBufferUtil; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class ImageHeaderParserUtilsTest { private final List fakeParsers = Arrays.asList(new FakeImageHeaderParser(), new FakeImageHeaderParser()); private List parsers; private final Context context = ApplicationProvider.getApplicationContext(); private final byte[] expectedData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8}; private final LruArrayPool lruArrayPool = new LruArrayPool(); @Before public void setUp() { parsers = new ArrayList(); for (FakeImageHeaderParser parser : fakeParsers) { parsers.add(parser); } } @Test public void getType_withTwoParsers_andStream_rewindsBeforeEachParser() throws IOException { ImageHeaderParserUtils.getType(parsers, new ByteArrayInputStream(expectedData), lruArrayPool); assertAllParsersReceivedTheSameData(); } @Test public void getType_withTwoParsers_andByteBuffer_rewindsBeforeEachParser() throws IOException { ImageHeaderParserUtils.getType(parsers, ByteBuffer.wrap(expectedData)); assertAllParsersReceivedTheSameData(); } @Test public void getType_withTwoParsers_andFileDescriptor_rewindsBeforeEachParser() throws IOException { // This test can't work if file descriptor rewinding isn't supported. Sadly that means this // test doesn't work in Robolectric. assumeTrue(ParcelFileDescriptorRewinder.isSupported()); ParcelFileDescriptor fileDescriptor = null; try { fileDescriptor = asFileDescriptor(expectedData); ParcelFileDescriptorRewinder rewinder = new ParcelFileDescriptorRewinder(fileDescriptor); ImageHeaderParserUtils.getType(parsers, rewinder, lruArrayPool); } finally { if (fileDescriptor != null) { fileDescriptor.close(); } } assertAllParsersReceivedTheSameData(); } @Test public void getOrientation_withTwoParsers_andStream_rewindsBeforeEachParser() throws IOException { ImageHeaderParserUtils.getOrientation( parsers, new ByteArrayInputStream(expectedData), lruArrayPool); assertAllParsersReceivedTheSameData(); } @Test public void getOrientation_withTwoParsers_andByteBuffer_rewindsBeforeEachParser() throws IOException { ImageHeaderParserUtils.getOrientation(parsers, ByteBuffer.wrap(expectedData), lruArrayPool); assertAllParsersReceivedTheSameData(); } @Test public void getOrientation_withTwoParsers_andFileDescriptor_rewindsBeforeEachParser() throws IOException { // This test can't work if file descriptor rewinding isn't supported. Sadly that means this // test doesn't work in Robolectric. assumeTrue(ParcelFileDescriptorRewinder.isSupported()); ParcelFileDescriptor fileDescriptor = null; try { fileDescriptor = asFileDescriptor(expectedData); ParcelFileDescriptorRewinder rewinder = new ParcelFileDescriptorRewinder(fileDescriptor); ImageHeaderParserUtils.getOrientation(parsers, rewinder, lruArrayPool); } finally { if (fileDescriptor != null) { fileDescriptor.close(); } } assertAllParsersReceivedTheSameData(); } @Test public void hasJpegMpf_withTwoParsers_andStream_rewindsBeforeEachParser() throws IOException { ImageHeaderParserUtils.hasJpegMpf( parsers, new ByteArrayInputStream(expectedData), lruArrayPool); assertAllParsersReceivedTheSameData(); } @Test public void hasJpegMpf_withTwoParsers_andByteBuffer_rewindsBeforeEachParser() throws IOException { ImageHeaderParserUtils.hasJpegMpf(parsers, ByteBuffer.wrap(expectedData), lruArrayPool); assertAllParsersReceivedTheSameData(); } @Test public void hasJpegMpf_withTwoParsers_andFileDescriptor_rewindsBeforeEachParser() throws IOException { // This test can't work if file descriptor rewinding isn't supported. Sadly that means this // test doesn't work in Robolectric. assumeTrue(ParcelFileDescriptorRewinder.isSupported()); ParcelFileDescriptor fileDescriptor = null; try { fileDescriptor = asFileDescriptor(expectedData); ParcelFileDescriptorRewinder rewinder = new ParcelFileDescriptorRewinder(fileDescriptor); ImageHeaderParserUtils.hasJpegMpf(parsers, rewinder, lruArrayPool); } finally { if (fileDescriptor != null) { fileDescriptor.close(); } } assertAllParsersReceivedTheSameData(); } private void assertAllParsersReceivedTheSameData() { for (FakeImageHeaderParser parser : fakeParsers) { assertThat(parser.data).isNotNull(); assertThat(parser.data).asList().containsExactlyElementsIn(asList(expectedData)).inOrder(); } } private static List asList(byte[] data) { List result = new ArrayList<>(); for (byte item : data) { result.add(item); } return result; } private ParcelFileDescriptor asFileDescriptor(byte[] data) throws IOException { File file = new File(context.getCacheDir(), "temp"); OutputStream os = null; try { os = new FileOutputStream(file); os.write(data); os.close(); } finally { if (os != null) { os.close(); } } return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } private static final class FakeImageHeaderParser implements ImageHeaderParser { private byte[] data; private void readData(InputStream is) throws IOException { readData(ByteBufferUtil.fromStream(is)); } // This is rather roundabout, but it's a simple way of reading the remaining data in the buffer. private void readData(ByteBuffer byteBuffer) { byte[] data = new byte[byteBuffer.remaining()]; // A 0 length means we read no data. If we try to pass this to ByteBuffer it will throw. We'd // rather not get that obscure exception and instead have an assertion above trigger because // we didn't read enough data. So we work around the exception here if we have no data to // read. if (data.length != 0) { byteBuffer.get(data, byteBuffer.position(), byteBuffer.remaining()); } this.data = data; } @NonNull @Override public ImageType getType(@NonNull InputStream is) throws IOException { readData(is); return ImageType.UNKNOWN; } @NonNull @Override public ImageType getType(@NonNull ByteBuffer byteBuffer) throws IOException { readData(byteBuffer); return ImageType.UNKNOWN; } @Override public int getOrientation(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { readData(is); return ImageHeaderParser.UNKNOWN_ORIENTATION; } @Override public int getOrientation(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException { readData(byteBuffer); return ImageHeaderParser.UNKNOWN_ORIENTATION; } @Override public boolean hasJpegMpf(@NonNull InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { readData(is); return false; } @Override public boolean hasJpegMpf(@NonNull ByteBuffer byteBuffer, @NonNull ArrayPool byteArrayPool) throws IOException { readData(byteBuffer); return false; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/MultiTransformationTest.java ================================================ package com.bumptech.glide.load; import static com.bumptech.glide.tests.Util.anyContext; import static com.bumptech.glide.tests.Util.anyResource; import static com.bumptech.glide.tests.Util.mockResource; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) @SuppressWarnings("unchecked") public class MultiTransformationTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Transformation first; @Mock private Transformation second; @Mock private Resource initial; @Mock private Resource firstTransformed; @Mock private Resource secondTransformed; private Application context; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); doAnswer(new Util.WriteDigest("first")) .when(first) .updateDiskCacheKey(any(MessageDigest.class)); doAnswer(new Util.WriteDigest("second")) .when(second) .updateDiskCacheKey(any(MessageDigest.class)); } @Test public void testAppliesTransformationsInOrder() { final int width = 584; final int height = 768; MultiTransformation transformation = new MultiTransformation<>(first, second); when(first.transform(anyContext(), eq(initial), eq(width), eq(height))) .thenReturn(firstTransformed); when(second.transform(anyContext(), eq(firstTransformed), eq(width), eq(height))) .thenReturn(secondTransformed); assertEquals(secondTransformed, transformation.transform(context, initial, width, height)); } @Test public void testInitialResourceIsNotRecycled() { when(first.transform(anyContext(), anyResource(), anyInt(), anyInt())) .thenReturn(firstTransformed); MultiTransformation transformation = new MultiTransformation<>(first); transformation.transform(context, initial, 123, 456); verify(initial, never()).recycle(); } @Test public void testInitialResourceIsNotRecycledEvenIfReturnedByMultipleTransformations() { when(first.transform(anyContext(), anyResource(), anyInt(), anyInt())).thenReturn(initial); when(second.transform(anyContext(), anyResource(), anyInt(), anyInt())).thenReturn(initial); MultiTransformation transformation = new MultiTransformation<>(first, second); transformation.transform(context, initial, 1111, 2222); verify(initial, never()).recycle(); } @Test public void testInitialResourceIsNotRecycledIfReturnedByOneTransformationButNotByALaterTransformation() { when(first.transform(anyContext(), anyResource(), anyInt(), anyInt())).thenReturn(initial); when(second.transform(anyContext(), anyResource(), anyInt(), anyInt())) .thenReturn(mockResource()); MultiTransformation transformation = new MultiTransformation<>(first, second); transformation.transform(context, initial, 1, 2); verify(initial, never()).recycle(); } @Test public void testFinalResourceIsNotRecycled() { when(first.transform(anyContext(), anyResource(), anyInt(), anyInt())) .thenReturn(firstTransformed); MultiTransformation transformation = new MultiTransformation<>(first); transformation.transform(context, mockResource(), 111, 222); verify(firstTransformed, never()).recycle(); } @Test public void testIntermediateResourcesAreRecycled() { when(first.transform(anyContext(), anyResource(), anyInt(), anyInt())) .thenReturn(firstTransformed); when(second.transform(anyContext(), anyResource(), anyInt(), anyInt())) .thenReturn(secondTransformed); MultiTransformation transformation = new MultiTransformation<>(first, second); transformation.transform(context, mockResource(), 233, 454); verify(firstTransformed).recycle(); } @Test public void testEquals() throws NoSuchAlgorithmException { keyTester .addEquivalenceGroup(new MultiTransformation<>(first), new MultiTransformation<>(first)) .addEquivalenceGroup(new MultiTransformation<>(second)) .addEquivalenceGroup(new MultiTransformation<>(first, second)) .addEquivalenceGroup(new MultiTransformation<>(second, first)) .addRegressionTest( new MultiTransformation<>(first), "a7937b64b8caa58f03721bb6bacf5c78cb235febe0e70b1b84cd99541461a08e") .addRegressionTest( new MultiTransformation<>(first, second), "da83f63e1a473003712c18f5afc5a79044221943d1083c7c5a7ac7236d85e8d2") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/OptionsTest.java ================================================ package com.bumptech.glide.load; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import androidx.annotation.NonNull; import com.bumptech.glide.load.Option.CacheKeyUpdater; import com.bumptech.glide.tests.KeyTester; import java.nio.ByteBuffer; import java.security.MessageDigest; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class OptionsTest { @Rule public final KeyTester keyTester = new KeyTester(); @Test public void testEquals() { Option firstMemoryOption = Option.memory("firstKey"); Object firstValue = new Object(); Option secondMemoryOption = Option.memory("secondKey"); Object secondValue = new Object(); CacheKeyUpdater updater = new CacheKeyUpdater() { @Override public void update( @NonNull byte[] keyBytes, @NonNull Integer value, @NonNull MessageDigest messageDigest) { messageDigest.update(keyBytes); messageDigest.update(ByteBuffer.allocate(4).putInt(value).array()); } }; Option firstDiskOption = Option.disk("firstDisk", updater); Option secondDiskOption = Option.disk("secondDisk", updater); keyTester .addEquivalenceGroup(new Options(), new Options()) .addEquivalenceGroup( new Options().set(firstMemoryOption, firstValue), new Options().set(firstMemoryOption, firstValue)) .addEquivalenceGroup( new Options().set(secondMemoryOption, secondValue), new Options().set(secondMemoryOption, secondValue)) .addEquivalenceGroup( new Options().set(firstMemoryOption, firstValue).set(secondMemoryOption, secondValue), new Options().set(firstMemoryOption, firstValue).set(secondMemoryOption, secondValue), new Options().set(secondMemoryOption, secondValue).set(firstMemoryOption, firstValue)) .addEquivalenceGroup(new Options().set(firstMemoryOption, secondValue)) .addEquivalenceGroup(new Options().set(secondMemoryOption, firstValue)) .addEquivalenceGroup( new Options().set(firstDiskOption, 1), new Options().set(firstDiskOption, 1)) .addEquivalenceGroup( new Options().set(secondDiskOption, 1), new Options().set(secondDiskOption, 1)) .addEquivalenceGroup(new Options().set(firstDiskOption, 2)) .addEquivalenceGroup(new Options().set(secondDiskOption, 2)) .addEquivalenceGroup( new Options().set(firstDiskOption, 1).set(secondDiskOption, 2), new Options().set(secondDiskOption, 2).set(firstDiskOption, 1)) .addEmptyDigestRegressionTest(new Options().set(firstMemoryOption, firstValue)) .addEmptyDigestRegressionTest( new Options().set(firstMemoryOption, firstValue).set(secondMemoryOption, secondValue)) .addRegressionTest( new Options().set(firstDiskOption, 123), "3c87124d1a765dc3d566f947d536ef140a4aca645c0947f702356714855b4a8e") .addRegressionTest( new Options().set(firstDiskOption, 123).set(secondDiskOption, 123), "6697f654686c9a925905db3840e9c99944642c2b91d6200360d77639c1754d51") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/BufferedOutputStreamFuzzTest.java ================================================ package com.bumptech.glide.load.data; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; /** * Runs some tests based on a random seed that asserts the output of writing to our buffered stream * matches the output of writing to {@link java.io.ByteArrayOutputStream}. */ @RunWith(JUnit4.class) public class BufferedOutputStreamFuzzTest { private static final int TESTS = 500; private static final int BUFFER_SIZE = 10; private static final int WRITES_PER_TEST = 50; private static final int MAX_BYTES_PER_WRITE = BUFFER_SIZE * 6; private static final Random RANDOM = new Random(-3207167907493985134L); @Mock private ArrayPool arrayPool; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(arrayPool.get(anyInt(), eq(byte[].class))) .thenAnswer( new Answer() { @Override public byte[] answer(InvocationOnMock invocation) throws Throwable { int size = (Integer) invocation.getArguments()[0]; return new byte[size]; } }); } @Test public void runFuzzTest() throws IOException { for (int i = 0; i < TESTS; i++) { runTest(RANDOM); } } private void runTest(Random random) throws IOException { List writes = new ArrayList<>(WRITES_PER_TEST); for (int i = 0; i < WRITES_PER_TEST; i++) { WriteType writeType = getType(random); writes.add(getWrite(random, writeType)); } ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream wrapped = new ByteArrayOutputStream(); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(wrapped, arrayPool, BUFFER_SIZE); for (Write write : writes) { switch (write.writeType) { case BYTE: byteArrayOutputStream.write(write.data[0]); bufferedOutputStream.write(write.data[0]); break; case BUFFER: byteArrayOutputStream.write(write.data); bufferedOutputStream.write(write.data); break; case OFFSET_BUFFER: byteArrayOutputStream.write(write.data, write.offset, write.length); bufferedOutputStream.write(write.data, write.offset, write.length); break; default: throw new IllegalArgumentException(); } } byte[] fromByteArrayStream = byteArrayOutputStream.toByteArray(); bufferedOutputStream.close(); byte[] fromWrappedStream = wrapped.toByteArray(); if (!Arrays.equals(fromWrappedStream, fromByteArrayStream)) { StringBuilder writesBuilder = new StringBuilder(); for (Write write : writes) { writesBuilder.append(write).append("\n"); } fail( "Expected: " + Arrays.toString(fromByteArrayStream) + "\n" + "but got: " + Arrays.toString(fromWrappedStream) + "\n" + writesBuilder.toString()); } } private Write getWrite(Random random, WriteType type) { switch (type) { case BYTE: return getByteWrite(random); case BUFFER: return getBufferWrite(random); case OFFSET_BUFFER: return getOffsetBufferWrite(random); default: throw new IllegalArgumentException("Unrecognized type: " + type); } } private Write getOffsetBufferWrite(Random random) { int dataSize = random.nextInt(MAX_BYTES_PER_WRITE * 2); byte[] data = new byte[dataSize]; int length = dataSize == 0 ? 0 : random.nextInt(dataSize); int offset = dataSize - length <= 0 ? 0 : random.nextInt(dataSize - length); random.nextBytes(data); return new Write(data, length, offset, WriteType.OFFSET_BUFFER); } private Write getBufferWrite(Random random) { byte[] data = new byte[random.nextInt(MAX_BYTES_PER_WRITE)]; random.nextBytes(data); return new Write(data, /* length= */ data.length, /* offset= */ 0, WriteType.BUFFER); } private Write getByteWrite(Random random) { byte[] data = new byte[1]; random.nextBytes(data); return new Write(data, /* length= */ 1, /* offset= */ 0, WriteType.BYTE); } private WriteType getType(Random random) { return WriteType.values()[random.nextInt(WriteType.values().length)]; } private static final class Write { private final byte[] data; private final int length; private final int offset; private final WriteType writeType; @Override public String toString() { return "Write{" + "data=" + Arrays.toString(data) + ", length=" + length + ", offset=" + offset + ", writeType=" + writeType + '}'; } Write(byte[] data, int length, int offset, WriteType writeType) { this.data = data; this.length = length; this.offset = offset; this.writeType = writeType; } } private enum WriteType { BYTE, BUFFER, OFFSET_BUFFER } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/BufferedOutputStreamTest.java ================================================ package com.bumptech.glide.load.data; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(JUnit4.class) public class BufferedOutputStreamTest { @Mock private ArrayPool arrayPool; @Mock private OutputStream mockOutputStream; private final int bufferSize = 10; private final ByteArrayOutputStream inner = new ByteArrayOutputStream(); private int currentValue = 0; private BufferedOutputStream os; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(arrayPool.get(bufferSize, byte[].class)).thenReturn(new byte[bufferSize]); os = new BufferedOutputStream(inner, arrayPool, bufferSize); } @Test public void constructor_obtainsBufferFromArrayPool() { verify(arrayPool).get(bufferSize, byte[].class); } @Test public void close_returnsBufferObtainedFromConstructor() throws IOException { byte[] data = new byte[bufferSize]; when(arrayPool.get(bufferSize, byte[].class)).thenReturn(data); os = new BufferedOutputStream(inner, arrayPool, bufferSize); os.close(); verify(arrayPool).put(data); } @Test public void write_withEmptyBuffer_andSingleByte_doesNotWriteToStream() throws IOException { os.write(next()); assertThat(inner.toByteArray()).isEmpty(); } @Test public void write_withEmptyBuffer_andDataSmallerThanBuffer_doesNotWriteToStream() throws IOException { os.write(next(bufferSize - 1)); assertThat(inner.toByteArray()).isEmpty(); } @Test public void write_withEmptyBuffer_andDataWithOffsetSizeSmallerThanBuffer_doesNotWriteToStream() throws IOException { int offset = 1; int length = bufferSize - offset; byte[] data = nextWithOffset(offset, length); os.write(data, offset, length); assertThat(inner.toByteArray()).isEmpty(); } @Test public void write_withEmptyBuffer_andDataWithPaddingSizeSmallerThanBuffer_doesNotWriteToStream() throws IOException { int padding = 1; int length = bufferSize - padding; byte[] data = nextWithPadding(length, padding); os.write(data, 0, length); assertThat(inner.toByteArray()).isEmpty(); } @Test public void write_withEmptyBuffer_andDataEqualToBufferSize_writesDataToStream() throws IOException { os.write(next(bufferSize)); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withEmptyBuffer_andDataGreaterThanBufferSize_writesDataToStream() throws IOException { os.write(next(bufferSize + 1)); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withEmptyBuffer_andDataWithOffsetAndLengthEqualToBufferSize_writesDataToStream() throws IOException { int offset = 5; int length = bufferSize; byte[] data = nextWithOffset(offset, length); os.write(data, offset, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withEmptyBuffer_andDataWithPaddingAndLengthEqualToBufferSize_writesData() throws IOException { int padding = 5; int length = bufferSize; byte[] data = nextWithPadding(length, padding); os.write(data, 0, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withEmptyBuffer_andDataWithOffsetAndLengthGreaterThanBuffer_writesDataToStream() throws IOException { int offset = 5; int length = bufferSize + 1; byte[] data = nextWithOffset(offset, length); os.write(data, offset, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withEmptyBuffer_andDataWithPaddingAndLengthGreaterThanBuffer_writesData() throws IOException { int padding = 5; int length = bufferSize + 1; byte[] data = nextWithPadding(length, padding); os.write(data, 0, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void writeSingleByte_whenBufferAlmostFull_writesBufferToStream() throws IOException { for (int i = 0; i < bufferSize; i++) { os.write(next()); } assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_withSingleByteInBuffer_writesBufferToStream() throws IOException { os.write(next()); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_afterWritingByteAfterBufferFull_writesByteToStream() throws IOException { for (int i = 0; i < bufferSize; i++) { os.write(next()); } os.write(next()); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flushAfterPreviousFlush_withSingleByte_writesOnlySingleByte() throws IOException { os.write(next()); os.flush(); os.write(next()); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_withSingleByteInBuffer_writesBufferToStream() throws IOException { os.write(next()); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWritingByteAfterBufferFull_writesByteToStream() throws IOException { for (int i = 0; i < bufferSize; i++) { os.write(next()); } os.write(next()); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void closeAfterPreviousFlush_withSingleByte_writesOnlySingleByte() throws IOException { os.write(next()); os.flush(); os.write(next()); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withDataInBuffer_bufferLessThanRemaining_doesNotWriteToStream() throws IOException { os.write(next()); os.write(next(remaining() - 1)); assertThat(inner.toByteArray()).isEmpty(); } @Test public void flush_afterWriteWithDataInBuffer_bufferLessThanRemaining_writesToStream() throws IOException { os.write(next()); byte[] data = next(remaining() - 1); os.write(data); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteWithDataInBuffer_bufferLessThanRemaining_writesToStream() throws IOException { os.write(next()); byte[] data = next(remaining()); os.write(data); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withBufferEqualToRemaining_lessThanLength_writesToStream() throws IOException { os.write(next()); os.write(next(remaining())); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_afterWriteBufferEqualToRemaining_doesNothing() throws IOException { os.write(next()); os.write(next(remaining())); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteBufferEqualToRemaining_doesNothing() throws IOException { os.write(next()); os.write(next(remaining())); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withOffsetBufferEqualToRemaining_lessThanLength_writesToStream() throws IOException { os.write(next()); int offset = 5; int length = remaining(); os.write(nextWithOffset(offset, length), offset, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_afterWriteOffsetBufferEqualToRemaining_lessThanLength_writesToStream() throws IOException { os.write(next()); int offset = 5; int length = remaining(); os.write(nextWithOffset(offset, length), offset, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteOffsetBufferEqualToRemaining_lessThanLength_writesToStream() throws IOException { os.write(next()); int offset = 5; int length = remaining(); os.write(nextWithOffset(offset, length), offset, length); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withPaddedBufferEqualToRemaining_lessThanLength_writesToStream() throws IOException { os.write(next()); int padding = 5; int length = remaining(); os.write(nextWithPadding(length, padding), 0, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_afterWritePaddedBufferEqualToRemaining_lessThanLength_writesToStream() throws IOException { os.write(next()); int padding = 5; int length = remaining(); os.write(nextWithPadding(length, padding), 0, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWritePaddedBufferEqualToRemaining_lessThanLength_writesToStream() throws IOException { os.write(next()); int padding = 5; int length = remaining(); os.write(nextWithPadding(length, padding), 0, length); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withBufferGreaterThanRemaining_lessThanLength_writesUpToBufferToStream() throws IOException { os.write(next(2)); os.write(next(bufferSize - 1)); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWriteBufferGreaterThanRemaining_lessThanLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize - 1)); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteBufferGreaterThanRemaining_lessThanLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize - 1)); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withOffsetBufferGreaterThanRemaining_lessThanLength_writesUpToBuffer() throws IOException { os.write(next(2)); int offset = 5; int length = bufferSize - 1; os.write(nextWithOffset(offset, length), offset, length); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWriteOffsetBufferGreaterThanRemaining_lessThanLength_writesAll() throws IOException { os.write(next(2)); int offset = 5; int length = bufferSize - 1; os.write(nextWithOffset(offset, length), offset, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteOffsetBufferGreaterThanRemaining_lessThanLength_writesAll() throws IOException { os.write(next(2)); int offset = 5; int length = bufferSize - 1; os.write(nextWithOffset(offset, length), offset, length); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withPaddedBufferGreaterThanRemaining_lessThanLength_writesUpToBuffer() throws IOException { os.write(next(2)); int padding = 5; int length = bufferSize - 1; os.write(nextWithPadding(length, padding), 0, length); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWritePaddedBufferGreaterThanRemaining_lessThanLength_writesAll() throws IOException { os.write(next(2)); int padding = 5; int length = bufferSize - 1; os.write(nextWithPadding(length, padding), 0, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWritePaddedBufferGreaterThanRemaining_lessThanLength_writesAll() throws IOException { os.write(next(2)); int padding = 5; int length = bufferSize - 1; os.write(nextWithPadding(length, padding), 0, length); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withBufferGreaterThanRemaining_equalToLength_writesUpToBufferToStream() throws IOException { os.write(next(2)); os.write(next(bufferSize)); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWriteBufferGreaterThanRemaining_equalToLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize)); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteBufferGreaterThanRemaining_equalToLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize)); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withOffsetBufferGreaterThanRemaining_equalToLength_writesUpToBufferToStream() throws IOException { os.write(next(2)); int offset = 6; int length = bufferSize; os.write(nextWithOffset(offset, length), offset, length); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWriteOffsetBufferGreaterThanRemaining_equalToLength_writesAll() throws IOException { os.write(next(2)); int offset = 6; int length = bufferSize; os.write(nextWithOffset(offset, length), offset, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteOffsetBufferGreaterThanRemaining_equalToLength_writesAll() throws IOException { os.write(next(2)); int offset = 6; int length = bufferSize; os.write(nextWithOffset(offset, length), offset, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withPaddedBufferGreaterThanRemaining_equalToLength_writesUpToBufferToStream() throws IOException { os.write(next(2)); int padding = 6; int length = bufferSize; os.write(nextWithPadding(length, padding), 0, length); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWritePaddedBufferGreaterThanRemaining_equalToLength_writesAll() throws IOException { os.write(next(2)); int padding = 6; int length = bufferSize; os.write(nextWithPadding(length, padding), 0, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWritePaddedBufferGreaterThanRemaining_equalToLength_writesAll() throws IOException { os.write(next(2)); int padding = 6; int length = bufferSize; os.write(nextWithPadding(length, padding), 0, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withBufferGreaterThanRemaining_greaterThanLength_writesUpToBufferToStream() throws IOException { os.write(next(2)); os.write(next(bufferSize + 1)); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWriteBufferGreaterThanRemaining_greaterThanLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize + 1)); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteBufferGreaterThanRemaining_greaterThanLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize + 1)); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withOffsetBufferGreaterThanRemaining_greaterThanLength_writesUpToBuffer() throws IOException { os.write(next(2)); int offset = 2; int length = bufferSize + 1; os.write(nextWithOffset(offset, length), offset, length); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWriteOffsetBufferGreaterThanRemaining_greaterThanLength_writesAllToStream() throws IOException { os.write(next(2)); int offset = 2; int length = bufferSize + 1; os.write(nextWithOffset(offset, length), offset, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteOffsetBufferGreaterThanRemaining_greaterThanLength_writesAllToStream() throws IOException { os.write(next(2)); int offset = 2; int length = bufferSize + 1; os.write(nextWithOffset(offset, length), offset, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withPaddedBufferGreaterThanRemaining_greaterThanLength_writesUpToBuffer() throws IOException { os.write(next(2)); int padding = 2; int length = bufferSize + 1; os.write(nextWithPadding(length, padding), 0, length); assertThat(inner.toByteArray()).isEqualTo(upTo(bufferSize)); } @Test public void flush_afterWritePaddedBufferGreaterThanRemaining_greaterThanLength_writesAllToStream() throws IOException { os.write(next(2)); int padding = 2; int length = bufferSize + 1; os.write(nextWithPadding(length, padding), 0, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWritePaddedBufferGreaterThanRemaining_greaterThanLength_writesAllToStream() throws IOException { os.write(next(2)); int padding = 2; int length = bufferSize + 1; os.write(nextWithPadding(length, padding), 0, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize * 2 + 1)); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_afterWriteBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize * 2 + 1)); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); os.write(next(bufferSize * 2 + 1)); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withOffsetBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); int offset = bufferSize + 1; int length = bufferSize * 2 + 2; os.write(nextWithOffset(offset, length), offset, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_afterWriteOffsetBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); int offset = bufferSize + 1; int length = bufferSize * 2 + 2; os.write(nextWithOffset(offset, length), offset, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWriteOffsetBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); int offset = bufferSize + 1; int length = bufferSize * 2 + 2; os.write(nextWithOffset(offset, length), offset, length); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_withPaddedBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); int padding = bufferSize + 1; int length = bufferSize * 2 + 2; os.write(nextWithPadding(length, padding), 0, length); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_afterWritePaddedBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); int padding = bufferSize + 1; int length = bufferSize * 2 + 2; os.write(nextWithPadding(length, padding), 0, length); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void close_afterWritePaddedBufferMoreThanRemains_greaterThanTwiceLength_writesAll() throws IOException { os.write(next(2)); int padding = bufferSize + 1; int length = bufferSize * 2 + 2; os.write(nextWithPadding(length, padding), 0, length); os.close(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void flush_flushesUnderlyingStream() throws IOException { os = new BufferedOutputStream(mockOutputStream, arrayPool, bufferSize); os.flush(); verify(mockOutputStream).flush(); } @Test public void overflowBuffer_doesNotFlushUnderlyingStream() throws IOException { os = new BufferedOutputStream(mockOutputStream, arrayPool, bufferSize); os.write(1); os.write(next(remaining() + 1)); verify(mockOutputStream, never()).flush(); } @Test public void close_closesUnderlyingStream() throws IOException { os = new BufferedOutputStream(mockOutputStream, arrayPool, bufferSize); os.close(); verify(mockOutputStream).close(); } @Test public void close_whenUnderlyingStreamThrows_closesStream() throws IOException { os = new BufferedOutputStream(mockOutputStream, arrayPool, bufferSize); doThrow(new IOException()).when(mockOutputStream).write(any(byte[].class), anyInt(), anyInt()); os.write(1); try { os.close(); fail("Failed to receive expected exception"); } catch (IOException e) { // Expected. } verify(mockOutputStream).close(); } @Test public void flush_withZeroBytesWritten_doesNotWriteToStream() throws IOException { os = new BufferedOutputStream(mockOutputStream, arrayPool, bufferSize); os.flush(); verify(mockOutputStream, never()).write(anyInt()); verify(mockOutputStream, never()).write(any(byte[].class)); verify(mockOutputStream, never()).write(any(byte[].class), anyInt(), anyInt()); } @Test public void write_throwsIfOffsetIsLessThanZero() { assertThrows( IndexOutOfBoundsException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { os.write(new byte[0], /* initialOffset= */ -1, /* length= */ 0); } }); } @Test public void write_throwsIfLengthIsLessThanZero() { assertThrows( IndexOutOfBoundsException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { os.write(new byte[0], /* initialOffset= */ 0, /* length= */ -1); } }); } @Test public void write_throwsIfOffsetIsGreaterThanLength() { assertThrows( IndexOutOfBoundsException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { os.write(new byte[0], /* initialOffset= */ 1, /* length= */ 0); } }); } @Test public void write_throwsIfLengthsIsGreaterThanLength() { assertThrows( IndexOutOfBoundsException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { os.write(new byte[0], /* initialOffset= */ 0, /* length= */ 1); } }); } @Test public void write_throwsIfLengthAndOffsetsIsGreaterThanLength() { assertThrows( IndexOutOfBoundsException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { os.write(new byte[1], /* initialOffset= */ 1, /* length= */ 1); } }); } @Test public void write_withZeroLengthBuffer_doesNothing() throws IOException { os.write(new byte[0]); assertThat(inner.toByteArray()).hasLength(0); } @Test public void write_withZeroLengthBufferAndZeroOffsetAndLength_doesNothing() throws IOException { os.write(new byte[0], 0, 0); assertThat(inner.toByteArray()).hasLength(0); } @Test public void write_afterWriteWithZeroLengthBuffer_writesExpected() throws IOException { os.write(new byte[0]); os.write(next()); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } @Test public void write_afterWriteZeroLengthBufferAndZeroOffsetAndLength_writesExpected() throws IOException { os.write(new byte[0], 0, 0); os.write(next()); os.flush(); assertThat(inner.toByteArray()).isEqualTo(all()); } private int soFar() { return currentValue; } private int remaining() { return bufferSize - soFar(); } private int next() { return nextWithOffset(0, 1)[0]; } private byte[] next(int count) { return nextWithOffset(0, count); } private byte[] nextWithPadding(int count, int padding) { byte[] result = new byte[count + padding]; for (int i = 0; i < count; i++) { result[i] = (byte) ++currentValue; } for (int i = count; i < count + padding; i++) { result[i] = (byte) (i + currentValue); } return result; } private byte[] nextWithOffset(int offset, int count) { byte[] result = new byte[offset + count]; for (int i = offset - 1; i >= 0; i--) { result[i] = (byte) -offset; } for (int i = offset; i < offset + count; i++) { result[i] = (byte) ++currentValue; } return result; } private byte[] upTo(int size) { assertThat(size).isLessThan(currentValue); byte[] result = new byte[size]; for (int i = 0; i < size; i++) { result[i] = (byte) (i + 1); } return result; } private byte[] all() { byte[] result = new byte[currentValue]; for (int i = 0; i < currentValue; i++) { result[i] = (byte) (i + 1); } return result; } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/ExifOrientationStreamTest.java ================================================ package com.bumptech.glide.load.data; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser; import com.bumptech.glide.testutil.TestResourceUtil; import java.io.IOException; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ExifOrientationStreamTest { private ArrayPool byteArrayPool; private InputStream openOrientationExample(boolean isLandscape, int item) { String filePrefix = isLandscape ? "Landscape" : "Portrait"; return TestResourceUtil.openResource(getClass(), filePrefix + "_" + item + ".jpg"); } @Before public void setUp() { byteArrayPool = new LruArrayPool(); } @Test public void testIncludesGivenExifOrientation() throws IOException { for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { InputStream toWrap = openOrientationExample(true /*isLandscape*/, j + 1); InputStream wrapped = new ExifOrientationStream(toWrap, i); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertThat(parser.getOrientation(wrapped, byteArrayPool)).isEqualTo(i); toWrap = openOrientationExample(false /*isLandscape*/, j + 1); wrapped = new ExifOrientationStream(toWrap, i); assertThat(parser.getOrientation(wrapped, byteArrayPool)).isEqualTo(i); } } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/FileDescriptorAssetPathFetcherTest.java ================================================ package com.bumptech.glide.load.data; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import com.bumptech.glide.Priority; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class FileDescriptorAssetPathFetcherTest { @Mock private AssetManager assetManager; @Mock private AssetFileDescriptor assetFileDescriptor; @Mock private DataFetcher.DataCallback callback; private FileDescriptorAssetPathFetcher fetcher; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); String assetPath = "/some/asset/path"; fetcher = new FileDescriptorAssetPathFetcher(assetManager, assetPath); when(assetManager.openFd(eq(assetPath))).thenReturn(assetFileDescriptor); } @Test public void testOpensInputStreamForPathWithAssetManager() throws Exception { fetcher.loadData(Priority.NORMAL, callback); verify(callback).onDataReady(eq(assetFileDescriptor)); } @Test public void testClosesOpenedInputStreamOnCleanup() throws Exception { fetcher.loadData(Priority.NORMAL, callback); fetcher.cleanup(); verify(assetFileDescriptor).close(); } @Test public void testDoesNothingOnCleanupIfNoDataLoaded() throws IOException { fetcher.cleanup(); verify(assetFileDescriptor, never()).close(); } @Test public void testDoesNothingOnCancel() throws Exception { fetcher.loadData(Priority.NORMAL, callback); fetcher.cancel(); verify(assetFileDescriptor, never()).close(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/HttpUrlFetcherServerTest.java ================================================ package com.bumptech.glide.load.data; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.bumptech.glide.Priority; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.Headers; import com.bumptech.glide.testutil.TestUtil; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; /** * Tests {@link com.bumptech.glide.load.data.HttpUrlFetcher} against server responses. Tests for * behavior (connection/disconnection/options) should go in {@link * com.bumptech.glide.load.data.HttpUrlFetcherTest}, response handling should go here. */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE, sdk = ROBOLECTRIC_SDK) public class HttpUrlFetcherServerTest { private static final String DEFAULT_PATH = "/fakepath"; private static final int TIMEOUT_TIME_MS = 300; @Mock private DataFetcher.DataCallback callback; private MockWebServer mockWebServer; private boolean defaultFollowRedirects; private ArgumentCaptor streamCaptor; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); defaultFollowRedirects = HttpURLConnection.getFollowRedirects(); HttpURLConnection.setFollowRedirects(false); mockWebServer = new MockWebServer(); mockWebServer.start(); streamCaptor = ArgumentCaptor.forClass(InputStream.class); } @After public void tearDown() throws IOException { HttpURLConnection.setFollowRedirects(defaultFollowRedirects); mockWebServer.shutdown(); } @Test public void testReturnsInputStreamOnStatusOk() throws Exception { String expected = "fakedata"; mockWebServer.enqueue(new MockResponse().setBody(expected).setResponseCode(200)); HttpUrlFetcher fetcher = getFetcher(); fetcher.loadData(Priority.HIGH, callback); verify(callback).onDataReady(streamCaptor.capture()); TestUtil.assertStreamOf(expected, streamCaptor.getValue()); assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET"); } @Test public void testHandlesRedirect301s() throws Exception { String expected = "fakedata"; mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url("/redirect").toString())); mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected)); getFetcher().loadData(Priority.LOW, callback); verify(callback).onDataReady(streamCaptor.capture()); TestUtil.assertStreamOf(expected, streamCaptor.getValue()); assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET"); assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET"); } @Test public void testHandlesRedirect302s() throws Exception { String expected = "fakedata"; mockWebServer.enqueue( new MockResponse() .setResponseCode(302) .setHeader("Location", mockWebServer.url("/redirect").toString())); mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected)); getFetcher().loadData(Priority.LOW, callback); verify(callback).onDataReady(streamCaptor.capture()); TestUtil.assertStreamOf(expected, streamCaptor.getValue()); assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET"); assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET"); } @Test public void testHandlesRelativeRedirects() throws Exception { String expected = "fakedata"; mockWebServer.enqueue( new MockResponse().setResponseCode(301).setHeader("Location", "/redirect")); mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected)); getFetcher().loadData(Priority.NORMAL, callback); verify(callback).onDataReady(streamCaptor.capture()); TestUtil.assertStreamOf(expected, streamCaptor.getValue()); RecordedRequest first = mockWebServer.takeRequest(); assertThat(first.getMethod()).isEqualTo("GET"); RecordedRequest second = mockWebServer.takeRequest(); assertThat(second.getPath()).endsWith("/redirect"); assertThat(second.getMethod()).isEqualTo("GET"); } @Test public void testHandlesUpToFiveRedirects() throws Exception { int numRedirects = 4; String expected = "redirectedData"; String redirectBase = "/redirect"; for (int i = 0; i < numRedirects; i++) { mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url(redirectBase + i).toString())); } mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected)); getFetcher().loadData(Priority.NORMAL, callback); verify(callback).onDataReady(streamCaptor.capture()); TestUtil.assertStreamOf(expected, streamCaptor.getValue()); RecordedRequest request = mockWebServer.takeRequest(); assertThat(request.getPath()).contains(DEFAULT_PATH); assertThat(request.getMethod()).isEqualTo("GET"); for (int i = 0; i < numRedirects; i++) { RecordedRequest current = mockWebServer.takeRequest(); assertThat(current.getPath()).contains(redirectBase + i); assertThat(current.getMethod()).isEqualTo("GET"); } } @Test public void testFailsOnRedirectLoops() throws Exception { mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url("/redirect").toString())); mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url("/redirect").toString())); getFetcher().loadData(Priority.IMMEDIATE, callback); verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testFailsIfRedirectLocationIsNotPresent() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(301)); getFetcher().loadData(Priority.NORMAL, callback); verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testFailsIfRedirectLocationIsPresentAndEmpty() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(301).setHeader("Location", "")); getFetcher().loadData(Priority.NORMAL, callback); verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testFailsIfStatusCodeIsNegativeOne() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(-1)); getFetcher().loadData(Priority.LOW, callback); verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testFailsAfterTooManyRedirects() throws Exception { for (int i = 0; i < 10; i++) { mockWebServer.enqueue( new MockResponse() .setResponseCode(301) .setHeader("Location", mockWebServer.url("/redirect" + i).toString())); } getFetcher().loadData(Priority.NORMAL, callback); verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testFailsIfStatusCodeIs500() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(500)); getFetcher().loadData(Priority.NORMAL, callback); verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testFailsIfStatusCodeIs400() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(400)); getFetcher().loadData(Priority.LOW, callback); verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testSetsReadTimeout() throws Exception { MockWebServer tempWebServer = new MockWebServer(); tempWebServer.enqueue( new MockResponse().setBody("test").throttleBody(1, TIMEOUT_TIME_MS, TimeUnit.MILLISECONDS)); tempWebServer.start(); try { getFetcher().loadData(Priority.HIGH, callback); } finally { tempWebServer.shutdown(); // shutdown() called before any enqueue() blocks until it times out. mockWebServer.enqueue(new MockResponse().setResponseCode(200)); } verify(callback).onLoadFailed(isA(IOException.class)); } @Test public void testAppliesHeadersInGlideUrl() throws Exception { mockWebServer.enqueue(new MockResponse().setResponseCode(200)); String headerField = "field"; String headerValue = "value"; Map headersMap = new HashMap<>(); headersMap.put(headerField, headerValue); Headers headers = mock(Headers.class); when(headers.getHeaders()).thenReturn(headersMap); getFetcher(headers).loadData(Priority.HIGH, callback); assertThat(mockWebServer.takeRequest().getHeader(headerField)).isEqualTo(headerValue); } private HttpUrlFetcher getFetcher() { return getFetcher(Headers.DEFAULT); } private HttpUrlFetcher getFetcher(Headers headers) { URL url = mockWebServer.url(DEFAULT_PATH).url(); return new HttpUrlFetcher( new GlideUrl(url, headers), TIMEOUT_TIME_MS, HttpUrlFetcher.DEFAULT_CONNECTION_FACTORY); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/HttpUrlFetcherTest.java ================================================ package com.bumptech.glide.load.data; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.bumptech.glide.Priority; import com.bumptech.glide.load.HttpException; import com.bumptech.glide.load.model.GlideUrl; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class HttpUrlFetcherTest { @Mock private HttpURLConnection urlConnection; @Mock private HttpUrlFetcher.HttpUrlConnectionFactory connectionFactory; @Mock private GlideUrl glideUrl; @Mock private InputStream stream; @Mock private DataFetcher.DataCallback callback; private static final int TIMEOUT_MS = 100; private HttpUrlFetcher fetcher; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); URL url = new URL("http://www.google.com"); when(connectionFactory.build(eq(url))).thenReturn(urlConnection); when(urlConnection.getInputStream()).thenReturn(stream); when(urlConnection.getResponseCode()).thenReturn(200); when(glideUrl.toURL()).thenReturn(url); fetcher = new HttpUrlFetcher(glideUrl, TIMEOUT_MS, connectionFactory); } @Test public void loadData_whenConnectThrowsFileNotFound_notifiesCallbackWithHttpErrorCode() throws IOException { int statusCode = 400; doThrow(new FileNotFoundException()).when(urlConnection).connect(); when(urlConnection.getResponseCode()).thenReturn(statusCode); fetcher.loadData(Priority.HIGH, callback); HttpException exception = (HttpException) getCallbackException(); assertThat(exception.getStatusCode()).isEqualTo(statusCode); } @Test public void loadData_whenGetInputStreamThrows_notifiesCallbackWithStatusCode() throws IOException { int statusCode = 400; doThrow(new IOException()).when(urlConnection).getInputStream(); when(urlConnection.getResponseCode()).thenReturn(statusCode); fetcher.loadData(Priority.HIGH, callback); HttpException exception = (HttpException) getCallbackException(); assertThat(exception.getStatusCode()).isEqualTo(statusCode); } @Test public void loadData_whenConnectAndGetResponseCodeThrow_notifiesCallbackWithInvalidStatusCode() throws IOException { doThrow(new FileNotFoundException()).when(urlConnection).connect(); when(urlConnection.getResponseCode()).thenThrow(new IOException()); fetcher.loadData(Priority.HIGH, callback); HttpException exception = (HttpException) getCallbackException(); assertThat(exception.getStatusCode()).isEqualTo(HttpUrlFetcher.INVALID_STATUS_CODE); } @Test public void loadData_whenRedirectUrlIsMalformed_notifiesCallbackWithStatusCode() throws IOException { int statusCode = 300; when(urlConnection.getHeaderField(eq(HttpUrlFetcher.REDIRECT_HEADER_FIELD))) .thenReturn("gg://www.google.com"); when(urlConnection.getResponseCode()).thenReturn(statusCode); fetcher.loadData(Priority.HIGH, callback); HttpException exception = (HttpException) getCallbackException(); assertThat(exception.getStatusCode()).isEqualTo(statusCode); assertThat(exception.getCause()).isInstanceOf(MalformedURLException.class); } private Exception getCallbackException() { ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); verify(callback).onLoadFailed(captor.capture()); return captor.getValue(); } @Test public void testSetsReadTimeout() { fetcher.loadData(Priority.HIGH, callback); verify(urlConnection).setReadTimeout(eq(TIMEOUT_MS)); } @Test public void testSetsConnectTimeout() { fetcher.loadData(Priority.IMMEDIATE, callback); verify(urlConnection).setConnectTimeout(eq(TIMEOUT_MS)); } @Test public void testReturnsNullIfCancelledBeforeConnects() throws IOException { InputStream notExpected = new ByteArrayInputStream(new byte[0]); when(urlConnection.getInputStream()).thenReturn(notExpected); fetcher.cancel(); fetcher.loadData(Priority.LOW, callback); verify(callback).onDataReady(ArgumentMatchers.isNull()); } @Test public void testDisconnectsUrlOnCleanup() { fetcher.loadData(Priority.HIGH, callback); fetcher.cleanup(); verify(urlConnection).disconnect(); } @Test public void testDoesNotThrowIfCleanupCalledBeforeStarted() { fetcher.cleanup(); } @Test public void testDoesNotThrowIfCancelCalledBeforeStart() { fetcher.cancel(); } @Test public void testCancelDoesNotDisconnectIfAlreadyConnected() { fetcher.loadData(Priority.HIGH, callback); fetcher.cancel(); verify(urlConnection, never()).disconnect(); } @Test public void testClosesStreamInCleanupIfNotNull() throws IOException { fetcher.loadData(Priority.HIGH, callback); fetcher.cleanup(); verify(stream).close(); } @Test public void testClosesStreamBeforeDisconnectingConnection() throws IOException { fetcher.loadData(Priority.NORMAL, callback); fetcher.cleanup(); InOrder order = inOrder(stream, urlConnection); order.verify(stream).close(); order.verify(urlConnection).disconnect(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/LocalUriFetcherTest.java ================================================ package com.bumptech.glide.load.data; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Priority; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class LocalUriFetcherTest { private TestLocalUriFetcher fetcher; @Mock private DataFetcher.DataCallback callback; @Before public void setUp() { MockitoAnnotations.initMocks(this); fetcher = new TestLocalUriFetcher( ApplicationProvider.getApplicationContext(), Uri.parse("content://empty")); } @Test public void testClosesDataOnCleanup() throws Exception { fetcher.loadData(Priority.NORMAL, callback); fetcher.cleanup(); verify(fetcher.closeable).close(); } @Test public void testDoesNotCloseNullData() throws IOException { fetcher.cleanup(); verify(fetcher.closeable, never()).close(); } @Test public void testHandlesExceptionOnClose() throws Exception { fetcher.loadData(Priority.NORMAL, callback); doThrow(new IOException("Test")).when(fetcher.closeable).close(); fetcher.cleanup(); verify(fetcher.closeable).close(); } private static class TestLocalUriFetcher extends LocalUriFetcher { final Closeable closeable = mock(Closeable.class); TestLocalUriFetcher(Context context, Uri uri) { super(context.getContentResolver(), uri); } @Override protected Closeable loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { return closeable; } @Override protected void close(Closeable data) throws IOException { data.close(); } @NonNull @Override public Class getDataClass() { return Closeable.class; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/StreamAssetPathFetcherTest.java ================================================ package com.bumptech.glide.load.data; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.AssetManager; import com.bumptech.glide.Priority; import java.io.IOException; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class StreamAssetPathFetcherTest { @Mock private AssetManager assetManager; @Mock private InputStream expected; @Mock private DataFetcher.DataCallback callback; private StreamAssetPathFetcher fetcher; @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); String assetPath = "/some/asset/path"; fetcher = new StreamAssetPathFetcher(assetManager, assetPath); when(assetManager.open(eq(assetPath))).thenReturn(expected); } @Test public void testOpensInputStreamForPathWithAssetManager() throws Exception { fetcher.loadData(Priority.NORMAL, callback); verify(callback).onDataReady(eq(expected)); } @Test public void testClosesOpenedInputStreamOnCleanup() throws Exception { fetcher.loadData(Priority.NORMAL, callback); fetcher.cleanup(); verify(expected).close(); } @Test public void testDoesNothingOnCleanupIfNoDataLoaded() throws IOException { fetcher.cleanup(); verify(expected, never()).close(); } @Test public void testDoesNothingOnCancel() throws Exception { fetcher.loadData(Priority.NORMAL, callback); fetcher.cancel(); verify(expected, never()).close(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/mediastore/MediaStoreUtilTest.java ================================================ package com.bumptech.glide.load.data.mediastore; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import android.provider.MediaStore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class MediaStoreUtilTest { @Test public void isAndroidPickerUri_notAndroidPickerUri_returnsFalse() { Uri mediaStoreUri = Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "123"); assertThat(MediaStoreUtil.isAndroidPickerUri(mediaStoreUri)).isFalse(); } @Test public void isAndroidPickerUri_identifiesAndroidPickerUri_returnsTrue() { Uri androidPickerUri = Uri.parse("content://media/picker/0/com.android.providers.media.photopicker/media/123"); assertThat(MediaStoreUtil.isAndroidPickerUri(androidPickerUri)).isTrue(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/mediastore/ThumbFetcherTest.java ================================================ package com.bumptech.glide.load.data.mediastore; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.net.Uri; import android.provider.MediaStore; import com.bumptech.glide.Priority; import com.bumptech.glide.load.data.DataFetcher; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ThumbFetcherTest { @Mock private ThumbnailStreamOpener opener; @Mock private DataFetcher.DataCallback callback; @Mock private InputStream expected; private ThumbFetcher fetcher; private Uri uri; @Before public void setUp() { MockitoAnnotations.initMocks(this); uri = Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "123"); fetcher = new ThumbFetcher(uri, opener); } @Test public void testReturnsInputStreamFromThumbnailOpener() throws Exception { when(opener.open(eq(uri))).thenReturn(expected); fetcher.loadData(Priority.LOW, callback); verify(callback).onDataReady(ArgumentMatchers.isNotNull()); } @Test public void testClosesInputStreamFromThumbnailOpenerOnCleanup() throws Exception { when(opener.open(eq(uri))).thenReturn(expected); fetcher.loadData(Priority.HIGH, callback); fetcher.cleanup(); verify(expected).close(); } @Test public void testDoesNotThrowIfCleanupWithNullInputStream() { fetcher.cleanup(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/mediastore/ThumbnailStreamOpenerTest.java ================================================ package com.bumptech.glide.load.data.mediastore; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.ContentResolver; import android.database.MatrixCursor; import android.net.Uri; import android.provider.MediaStore; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.fakes.RoboCursor; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ThumbnailStreamOpenerTest { private Harness harness; @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Before public void setUp() throws Exception { harness = new Harness(); } @Test public void testReturnsNullIfCursorIsNull() throws FileNotFoundException { when(harness.query.query(eq(harness.uri))).thenReturn(null); assertNull(harness.get().open(harness.uri)); } @Test public void testReturnsNullIfCursorIsEmpty() throws FileNotFoundException { when(harness.query.query(eq(harness.uri))).thenReturn(new MatrixCursor(new String[1])); assertNull(harness.get().open(harness.uri)); } @Test public void testReturnsNullIfCursorHasEmptyPath() throws FileNotFoundException { MatrixCursor cursor = new MatrixCursor(new String[1]); cursor.addRow(new Object[] {""}); when(harness.query.query(eq(harness.uri))).thenReturn(cursor); assertNull(harness.get().open(harness.uri)); } @Test public void testReturnsNullIfFileDoesNotExist() throws FileNotFoundException { when(harness.service.get(anyString())).thenReturn(harness.file); when(harness.service.exists(eq(harness.file))).thenReturn(false); assertNull(harness.get().open(harness.uri)); } @Test public void testReturnNullIfFileLengthIsZero() throws FileNotFoundException { when(harness.service.get(anyString())).thenReturn(harness.file); when(harness.service.length(eq(harness.file))).thenReturn(0L); assertNull(harness.get().open(harness.uri)); } @Test public void testClosesCursor() throws FileNotFoundException { harness.get().open(harness.uri); assertTrue(harness.cursor.isClosed()); } @Test public void testReturnsOpenedInputStreamWhenFileFound() throws FileNotFoundException { InputStream expected = new ByteArrayInputStream(new byte[0]); Shadows.shadowOf(ApplicationProvider.getApplicationContext().getContentResolver()) .registerInputStream(harness.uri, expected); assertEquals(expected, harness.get().open(harness.uri)); } @Test public void open_returnsNull_whenQueryThrowsSecurityException() throws FileNotFoundException { when(harness.query.query(any(Uri.class))).thenThrow(new SecurityException()); assertThat(harness.get().open(harness.uri)).isNull(); } @Test public void testVideoQueryReturnsVideoCursor() { Uri queryUri = MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI; ThumbFetcher.VideoThumbnailQuery query = new ThumbFetcher.VideoThumbnailQuery(getContentResolver()); RoboCursor testCursor = new RoboCursor(); Shadows.shadowOf(ApplicationProvider.getApplicationContext().getContentResolver()) .setCursor(queryUri, testCursor); assertEquals(testCursor, query.query(harness.uri)); } @Test public void testImageQueryReturnsImageCursor() { Uri queryUri = MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI; ThumbFetcher.ImageThumbnailQuery query = new ThumbFetcher.ImageThumbnailQuery(getContentResolver()); RoboCursor testCursor = new RoboCursor(); Shadows.shadowOf(ApplicationProvider.getApplicationContext().getContentResolver()) .setCursor(queryUri, testCursor); assertEquals(testCursor, query.query(harness.uri)); } private static ContentResolver getContentResolver() { return ApplicationProvider.getApplicationContext().getContentResolver(); } private class Harness { final MatrixCursor cursor = new MatrixCursor(new String[1]); final File file = temporaryFolder.newFile(); final Uri uri = Uri.fromFile(file); final ThumbnailQuery query = mock(ThumbnailQuery.class); final FileService service = mock(FileService.class); final ArrayPool byteArrayPool = new LruArrayPool(); Harness() throws Exception { cursor.addRow(new String[] {file.getAbsolutePath()}); when(query.query(eq(uri))).thenReturn(cursor); when(service.get(eq(file.getAbsolutePath()))).thenReturn(file); when(service.exists(eq(file))).thenReturn(true); when(service.length(eq(file))).thenReturn(1L); } public ThumbnailStreamOpener get() { List parsers = new ArrayList<>(); parsers.add(new DefaultImageHeaderParser()); return new ThumbnailStreamOpener( parsers, service, query, byteArrayPool, getContentResolver()); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/resource/FileDescriptorLocalUriFetcherTest.java ================================================ package com.bumptech.glide.load.data.resource; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.ParcelFileDescriptor; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Priority; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.FileDescriptorLocalUriFetcher; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import com.bumptech.glide.tests.ContentResolverShadow; import java.io.FileNotFoundException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; @RunWith(RobolectricTestRunner.class) @Config( sdk = ROBOLECTRIC_SDK, shadows = {ContentResolverShadow.class}) public class FileDescriptorLocalUriFetcherTest { @Mock private DataFetcher.DataCallback callback; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void testLoadResource_returnsFileDescriptor() throws Exception { Context context = ApplicationProvider.getApplicationContext(); Uri uri = Uri.parse("file://nothing"); ContentResolver contentResolver = context.getContentResolver(); ContentResolverShadow shadow = Shadow.extract(contentResolver); AssetFileDescriptor assetFileDescriptor = mock(AssetFileDescriptor.class); ParcelFileDescriptor parcelFileDescriptor = mock(ParcelFileDescriptor.class); when(assetFileDescriptor.getParcelFileDescriptor()).thenReturn(parcelFileDescriptor); shadow.registerFileDescriptor(uri, assetFileDescriptor); FileDescriptorLocalUriFetcher fetcher = new FileDescriptorLocalUriFetcher(context.getContentResolver(), uri, false); fetcher.loadData(Priority.NORMAL, callback); verify(callback).onDataReady(eq(parcelFileDescriptor)); } @Test public void testLoadResource_mediaUri_returnsFileDescriptor() throws Exception { Context context = ApplicationProvider.getApplicationContext(); Uri uri = Uri.parse("content://media"); ContentResolver contentResolver = context.getContentResolver(); AssetFileDescriptor assetFileDescriptor = mock(AssetFileDescriptor.class); ParcelFileDescriptor parcelFileDescriptor = mock(ParcelFileDescriptor.class); when(assetFileDescriptor.getParcelFileDescriptor()).thenReturn(parcelFileDescriptor); FileDescriptorLocalUriFetcher fetcher = new FileDescriptorLocalUriFetcher( context.getContentResolver(), uri, /* useMediaStoreApisIfAvailable */ true); try (MockedStatic utils = Mockito.mockStatic(MediaStoreUtil.class)) { utils.when(MediaStoreUtil::isMediaStoreOpenFileApisAvailable).thenReturn(true); utils.when(() -> MediaStoreUtil.isMediaStoreUri(uri)).thenReturn(true); utils .when(() -> MediaStoreUtil.openAssetFileDescriptor(uri, contentResolver)) .thenReturn(assetFileDescriptor); fetcher.loadData(Priority.NORMAL, callback); verify(callback).onDataReady(eq(parcelFileDescriptor)); } } @Test public void testLoadResource_withNullFileDescriptor_callsLoadFailed() { Context context = ApplicationProvider.getApplicationContext(); Uri uri = Uri.parse("file://nothing"); ContentResolver contentResolver = context.getContentResolver(); ContentResolverShadow shadow = Shadow.extract(contentResolver); shadow.registerFileDescriptor(uri, null /*fileDescriptor*/); FileDescriptorLocalUriFetcher fetcher = new FileDescriptorLocalUriFetcher( context.getContentResolver(), uri, /* useMediaStoreApisIfAvailable */ false); fetcher.loadData(Priority.NORMAL, callback); verify(callback).onLoadFailed(isA(FileNotFoundException.class)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/data/resource/StreamLocalUriFetcherTest.java ================================================ package com.bumptech.glide.load.data.resource; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Priority; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.StreamLocalUriFetcher; import com.bumptech.glide.load.data.mediastore.MediaStoreUtil; import com.bumptech.glide.tests.ContentResolverShadow; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; @RunWith(RobolectricTestRunner.class) @Config( sdk = ROBOLECTRIC_SDK, shadows = {ContentResolverShadow.class}) public class StreamLocalUriFetcherTest { @Mock private DataFetcher.DataCallback callback; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void testLoadResource_returnsInputStream() throws Exception { Context context = ApplicationProvider.getApplicationContext(); Uri uri = Uri.parse("file://nothing"); ContentResolver contentResolver = context.getContentResolver(); ContentResolverShadow shadow = Shadow.extract(contentResolver); shadow.registerInputStream(uri, new ByteArrayInputStream(new byte[0])); StreamLocalUriFetcher fetcher = new StreamLocalUriFetcher(context.getContentResolver(), uri, false); fetcher.loadData(Priority.NORMAL, callback); verify(callback).onDataReady(ArgumentMatchers.isNotNull()); } @Test public void testLoadResource_mediaUri_returnsFileDescriptor() throws Exception { Context context = ApplicationProvider.getApplicationContext(); Uri uri = Uri.parse("content://media"); ContentResolver contentResolver = context.getContentResolver(); AssetFileDescriptor assetFileDescriptor = mock(AssetFileDescriptor.class); FileInputStream inputStream = mock(FileInputStream.class); when(assetFileDescriptor.createInputStream()).thenReturn(inputStream); StreamLocalUriFetcher fetcher = new StreamLocalUriFetcher( context.getContentResolver(), uri, /* useMediaStoreApisIfAvailable */ true); try (MockedStatic utils = Mockito.mockStatic(MediaStoreUtil.class)) { utils.when(MediaStoreUtil::isMediaStoreOpenFileApisAvailable).thenReturn(true); utils.when(() -> MediaStoreUtil.isMediaStoreUri(uri)).thenReturn(true); utils .when(() -> MediaStoreUtil.openAssetFileDescriptor(uri, contentResolver)) .thenReturn(assetFileDescriptor); fetcher.loadData(Priority.NORMAL, callback); verify(callback).onDataReady(eq(inputStream)); } } @Test public void testLoadResource_withNullInputStream_callsLoadFailed() { Context context = ApplicationProvider.getApplicationContext(); Uri uri = Uri.parse("file://nothing"); ContentResolver contentResolver = context.getContentResolver(); ContentResolverShadow shadow = Shadow.extract(contentResolver); shadow.registerInputStream(uri, null /*inputStream*/); StreamLocalUriFetcher fetcher = new StreamLocalUriFetcher( context.getContentResolver(), uri, /* useMediaStoreApisIfAvailable */ false); fetcher.loadData(Priority.LOW, callback); verify(callback).onLoadFailed(isA(FileNotFoundException.class)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/ActiveResourcesTest.java ================================================ package com.bumptech.glide.load.engine; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.os.Looper; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.ActiveResources.DequeuedResourceCallback; import com.bumptech.glide.load.engine.ActiveResources.ResourceWeakReference; import com.bumptech.glide.load.engine.EngineResource.ResourceListener; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; @RunWith(RobolectricTestRunner.class) public class ActiveResourcesTest { @Mock private ResourceListener listener; @Mock private Key key; @Mock private Resource resource; private ActiveResources resources; @Before public void setUp() { MockitoAnnotations.initMocks(this); resources = new ActiveResources(/* isActiveResourceRetentionAllowed= */ true); resources.setListener(listener); } @After public void tearDown() { resources.shutdown(); } @Test public void get_withMissingKey_returnsNull() { assertThat(resources.get(key)).isNull(); } @Test public void get_withActiveKey_returnsResource() { EngineResource expected = newCacheableEngineResource(); resources.activate(key, expected); assertThat(resources.get(key)).isEqualTo(expected); } @Test public void get_withDeactivatedKey_returnsNull() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); resources.deactivate(key); assertThat(resources.get(key)).isNull(); } @Test public void deactivate_withNotActiveKey_doesNotThrow() { resources.deactivate(key); } @Test public void get_withActiveAndClearedKey_returnsNull() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); resources.activeEngineResources.get(key).clear(); assertThat(resources.get(key)).isNull(); } @Test public void get_withActiveAndClearedKey_andCacheableResource_callsListenerWithWrappedResource() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); resources.activeEngineResources.get(key).clear(); resources.get(key); ArgumentCaptor> captor = getEngineResourceCaptor(); verify(listener).onResourceReleased(eq(key), captor.capture()); assertThat(captor.getValue().getResource()).isEqualTo(resource); } @Test public void get_withActiveAndClearedKey_andCacheableResource_callsListenerWithNotRecycleable() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); resources.activeEngineResources.get(key).clear(); resources.get(key); ArgumentCaptor> captor = getEngineResourceCaptor(); verify(listener).onResourceReleased(eq(key), captor.capture()); captor.getValue().recycle(); verify(resource, never()).recycle(); } @Test public void get_withActiveAndClearedKey_andCacheableResource_callsListenerWithCacheable() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); resources.activeEngineResources.get(key).clear(); resources.get(key); ArgumentCaptor> captor = getEngineResourceCaptor(); verify(listener).onResourceReleased(eq(key), captor.capture()); assertThat(captor.getValue().isMemoryCacheable()).isTrue(); } @Test public void get_withActiveAndClearedKey_andNotCacheableResource_doesNotCallListener() { EngineResource engineResource = newNonCacheableEngineResource(); resources.activate(key, engineResource); resources.activeEngineResources.get(key).clear(); resources.get(key); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } @Test public void queueIdle_afterResourceRemovedFromActive_doesNotCallListener() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); resources.deactivate(key); enqueueAndWaitForRef(weakRef); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } @Test public void queueIdle_withCacheableResourceInActive_callListener() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); enqueueAndWaitForRef(weakRef); ArgumentCaptor> captor = getEngineResourceCaptor(); verify(listener).onResourceReleased(eq(key), captor.capture()); EngineResource released = captor.getValue(); assertThat(released.getResource()).isEqualTo(resource); assertThat(released.isMemoryCacheable()).isTrue(); released.recycle(); verify(resource, never()).recycle(); } @Test public void queueIdle_withNotCacheableResourceInActive_doesNotCallListener() { EngineResource engineResource = newNonCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); weakRef.enqueue(); enqueueAndWaitForRef(weakRef); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } @Test public void queueIdle_withCacheableResourceInActive_removesResourceFromActive() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); enqueueAndWaitForRef(weakRef); assertThat(resources.get(key)).isNull(); } @Test public void queueIdle_withNotCacheableResourceInActive_removesResourceFromActive() { EngineResource engineResource = newNonCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); enqueueAndWaitForRef(weakRef); assertThat(resources.get(key)).isNull(); } @Test public void queueIdle_withQueuedReferenceRetrievedFromGet_notifiesListener() { EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); resources.get(key); enqueueAndWaitForRef(weakRef); ArgumentCaptor> captor = getEngineResourceCaptor(); verify(listener).onResourceReleased(eq(key), captor.capture()); assertThat(captor.getValue().getResource()).isEqualTo(resource); } @Test public void queueIdle_withQueuedReferenceRetrievedFromGetAndNotCacheable_doesNotNotifyListener() { EngineResource engineResource = newNonCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); CountDownLatch latch = getLatchForClearedRef(); weakRef.enqueue(); resources.get(key); waitForLatch(latch); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } @Test public void queueIdle_withQueuedReferenceDeactivated_doesNotNotifyListener() { final ExecutorService delegate = Executors.newSingleThreadExecutor(); try { final CountDownLatch blockExecutor = new CountDownLatch(1); resources = new ActiveResources( /* isActiveResourceRetentionAllowed= */ true, new Executor() { @Override public void execute(@NonNull final Runnable command) { delegate.execute( new Runnable() { @Override public void run() { try { blockExecutor.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } command.run(); } }); } }); resources.setListener(listener); EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); CountDownLatch latch = getLatchForClearedRef(); weakRef.enqueue(); resources.deactivate(key); blockExecutor.countDown(); waitForLatch(latch); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } finally { resources.shutdown(); com.bumptech.glide.util.Executors.shutdownAndAwaitTermination(delegate); } } @Test public void queueIdle_afterReferenceQueuedThenReactivated_doesNotNotifyListener() { final ExecutorService delegate = Executors.newSingleThreadExecutor(); try { final CountDownLatch blockExecutor = new CountDownLatch(1); resources = new ActiveResources( /* isActiveResourceRetentionAllowed= */ true, new Executor() { @Override public void execute(@NonNull final Runnable command) { delegate.execute( new Runnable() { @Override public void run() { try { blockExecutor.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } command.run(); } }); } }); resources.setListener(listener); EngineResource first = newCacheableEngineResource(); resources.activate(key, first); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); CountDownLatch latch = getLatchForClearedRef(); weakRef.enqueue(); EngineResource second = newCacheableEngineResource(); resources.activate(key, second); blockExecutor.countDown(); waitForLatch(latch); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } finally { resources.shutdown(); com.bumptech.glide.util.Executors.shutdownAndAwaitTermination(delegate); } } @Test public void activate_withNonCacheableResource_doesNotSaveResource() { EngineResource engineResource = newNonCacheableEngineResource(); resources.activate(key, engineResource); assertThat(resources.activeEngineResources.get(key).resource).isNull(); } @Test public void get_withActiveClearedKey_cacheableResource_retentionDisabled_doesNotCallListener() { resources = new ActiveResources(/* isActiveResourceRetentionAllowed= */ false); resources.setListener(listener); EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); resources.activeEngineResources.get(key).clear(); resources.get(key); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } @Test public void queueIdle_withQueuedReferenceRetrievedFromGet_retentionDisabled_doesNotNotify() { resources = new ActiveResources(/* isActiveResourceRetentionAllowed= */ false); resources.setListener(listener); EngineResource engineResource = newCacheableEngineResource(); resources.activate(key, engineResource); ResourceWeakReference weakRef = resources.activeEngineResources.get(key); CountDownLatch latch = getLatchForClearedRef(); weakRef.enqueue(); resources.get(key); waitForLatch(latch); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); } private void enqueueAndWaitForRef(ResourceWeakReference ref) { CountDownLatch latch = getLatchForClearedRef(); ref.enqueue(); waitForLatch(latch); } private void waitForLatch(CountDownLatch latch) { try { latch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); } Shadows.shadowOf(Looper.getMainLooper()).runToEndOfTasks(); } private CountDownLatch getLatchForClearedRef() { final CountDownLatch toWait = new CountDownLatch(1); resources.setDequeuedResourceCallback( new DequeuedResourceCallback() { @Override public void onResourceDequeued() { toWait.countDown(); } }); return toWait; } private EngineResource newCacheableEngineResource() { return new EngineResource<>( resource, /* isMemoryCacheable= */ true, /* isRecyclable= */ false, key, listener); } private EngineResource newNonCacheableEngineResource() { return new EngineResource<>( resource, /* isMemoryCacheable= */ false, /* isRecyclable= */ false, key, listener); } @SuppressWarnings("unchecked") private static ArgumentCaptor> getEngineResourceCaptor() { return (ArgumentCaptor>) (ArgumentCaptor) ArgumentCaptor.forClass(EngineResource.class); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/DataCacheKeyTest.java ================================================ package com.bumptech.glide.load.engine; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import com.bumptech.glide.load.Key; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util.WriteDigest; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(JUnit4.class) public class DataCacheKeyTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Key firstKey; @Mock private Key firstSignature; @Mock private Key secondKey; @Mock private Key secondSignature; @Before public void setUp() throws UnsupportedEncodingException { MockitoAnnotations.initMocks(this); doAnswer(new WriteDigest("firstKey")) .when(firstKey) .updateDiskCacheKey(any(MessageDigest.class)); doAnswer(new WriteDigest("firstSignature")) .when(firstSignature) .updateDiskCacheKey(any(MessageDigest.class)); doAnswer(new WriteDigest("secondKey")) .when(secondKey) .updateDiskCacheKey(any(MessageDigest.class)); doAnswer(new WriteDigest("secondSignature")) .when(secondSignature) .updateDiskCacheKey(any(MessageDigest.class)); } @Test public void testEqualsHashCodeDigest() throws NoSuchAlgorithmException { keyTester .addEquivalenceGroup( new DataCacheKey(firstKey, firstSignature), new DataCacheKey(firstKey, firstSignature)) .addEquivalenceGroup(new DataCacheKey(firstKey, secondSignature)) .addEquivalenceGroup(new DataCacheKey(secondKey, firstSignature)) .addEquivalenceGroup(new DataCacheKey(secondKey, secondSignature)) .addRegressionTest( new DataCacheKey(firstKey, firstSignature), "801d7440d65a0e7c9ad0097d417f346dac4d4c4d5630724110fa3f3fe66236d9") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/EngineJobTest.java ================================================ package com.bumptech.glide.load.engine; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.anyResource; import static com.bumptech.glide.tests.Util.isADataSource; import static com.bumptech.glide.tests.Util.mockResource; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Handler; import android.os.Looper; import androidx.core.util.Pools; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.EngineResource.ResourceListener; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.request.ResourceCallback; import com.bumptech.glide.util.Executors; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.InOrder; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class EngineJobTest { private EngineJobHarness harness; @Before public void setUp() { harness = new EngineJobHarness(); } @Test public void testOnResourceReadyPassedToCallbacks() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); ShadowLooper.runUiThreadTasks(); verify(harness.cb) .onResourceReady( harness.engineResource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); } @Test public void testListenerNotifiedJobCompleteOnOnResourceReady() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); ShadowLooper.runUiThreadTasks(); verify(harness.engineJobListener) .onEngineJobComplete(eq(job), eq(harness.key), eq(harness.engineResource)); } @Test public void testNotifiesAllCallbacksOnReady() { MultiCbHarness harness = new MultiCbHarness(); harness.job.start(harness.decodeJob); harness.job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); for (ResourceCallback cb : harness.cbs) { verify(cb) .onResourceReady( harness.engineResource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); } } @Test public void testNotifiesAllCallbacksOnException() { MultiCbHarness harness = new MultiCbHarness(); harness.job.start(harness.decodeJob); GlideException exception = new GlideException("test"); harness.job.onLoadFailed(exception); for (ResourceCallback cb : harness.cbs) { verify(cb).onLoadFailed(eq(exception)); } } @Test public void testAcquiresResourceOncePerCallback() { MultiCbHarness harness = new MultiCbHarness(); harness.job.start(harness.decodeJob); harness.job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); // Acquired once and then released while notifying. InOrder order = inOrder(harness.engineResource); order.verify(harness.engineResource, times(harness.numCbs + 1)).acquire(); order.verify(harness.engineResource, times(1)).release(); } @Test public void testListenerNotifiedJobCompleteOnException() { harness = new EngineJobHarness(); EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onLoadFailed(new GlideException("test")); ShadowLooper.runUiThreadTasks(); verify(harness.engineJobListener) .onEngineJobComplete( eq(job), eq(harness.key), ArgumentMatchers.>isNull()); } @Test public void testResourceIsCacheableWhenIsCacheableOnReady() { harness.isCacheable = true; EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); ShadowLooper.runUiThreadTasks(); verify(harness.factory) .build( anyResource(), eq(harness.isCacheable), eq(harness.key), eq(harness.resourceListener)); } @Test public void testResourceIsCacheableWhenNotIsCacheableOnReady() { harness.isCacheable = false; EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); ShadowLooper.runUiThreadTasks(); verify(harness.factory) .build( anyResource(), eq(harness.isCacheable), eq(harness.key), eq(harness.resourceListener)); } @Test public void testListenerNotifiedOfCancelOnCancel() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.cancel(); verify(harness.engineJobListener).onEngineJobCancelled(eq(job), eq(harness.key)); } @Test public void testOnResourceReadyNotDeliveredAfterCancel() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.cancel(); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); ShadowLooper.runUiThreadTasks(); verify(harness.cb, never()).onResourceReady(anyResource(), isADataSource(), anyBoolean()); } @Test public void testOnExceptionNotDeliveredAfterCancel() { harness = new EngineJobHarness(); EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.cancel(); job.onLoadFailed(new GlideException("test")); ShadowLooper.runUiThreadTasks(); verify(harness.cb, never()).onLoadFailed(any(GlideException.class)); } @Test public void testRemovingAllCallbacksCancelsRunner() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.removeCallback(harness.cb); assertTrue(job.isCancelled()); } @SuppressWarnings("unchecked") @Test public void removingSomeCallbacksDoesNotCancelRunner() { EngineJob job = harness.getJob(); job.addCallback(mockResourceCallback(), Executors.directExecutor()); job.removeCallback(harness.cb); assertFalse(job.isCancelled()); } @Test public void testResourceIsAcquiredOncePerConsumerAndOnceForCache() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); // Once while notifying and once for single callback. verify(harness.engineResource, times(2)).acquire(); } @Test public void testDoesNotNotifyCancelledIfCompletes() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); verify(harness.engineJobListener, never()).onEngineJobCancelled(eq(job), eq(harness.key)); } @Test public void testDoesNotNotifyCancelledIfAlreadyCancelled() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.cancel(); job.cancel(); verify(harness.engineJobListener, times(1)).onEngineJobCancelled(eq(job), eq(harness.key)); } @Test public void testDoesNotNotifyCancelledIfReceivedException() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onLoadFailed(new GlideException("test")); verify(harness.engineJobListener) .onEngineJobComplete( eq(job), eq(harness.key), ArgumentMatchers.>isNull()); verify(harness.engineJobListener, never()) .onEngineJobCancelled(any(EngineJob.class), any(Key.class)); } @Test public void testReleasesResourceIfCancelledOnReady() { Looper looper = harness.mainHandler.getLooper(); Shadows.shadowOf(looper).pause(); final EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.cancel(); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); verify(harness.resource).recycle(); } @Test public void testDoesNotAcquireOnceForMemoryCacheIfNotCacheable() { harness.isCacheable = false; EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); verify(harness.engineResource, times(2)).acquire(); } @Test public void testNotifiesNewCallbackOfResourceIfCallbackIsAddedDuringOnResourceReady() { final EngineJob job = harness.getJob(); final ResourceCallback existingCallback = mockResourceCallback(); final ResourceCallback newCallback = mockResourceCallback(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { job.addCallback(newCallback, Executors.directExecutor()); return null; } }) .when(existingCallback) .onResourceReady(anyResource(), isADataSource(), anyBoolean()); job.addCallback(existingCallback, Executors.directExecutor()); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); verify(newCallback) .onResourceReady( harness.engineResource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); } @Test public void testNotifiesNewCallbackOfExceptionIfCallbackIsAddedDuringOnException() { harness = new EngineJobHarness(); final EngineJob job = harness.getJob(); final ResourceCallback existingCallback = mockResourceCallback(); final ResourceCallback newCallback = mockResourceCallback(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { job.addCallback(newCallback, Executors.directExecutor()); return null; } }) .when(existingCallback) .onLoadFailed(any(GlideException.class)); GlideException exception = new GlideException("test"); job.addCallback(existingCallback, Executors.directExecutor()); job.start(harness.decodeJob); job.onLoadFailed(exception); verify(newCallback).onLoadFailed(eq(exception)); } @Test public void testRemovingCallbackDuringOnResourceReadyIsIgnoredIfCallbackHasAlreadyBeenCalled() { final EngineJob job = harness.getJob(); final ResourceCallback cb = mockResourceCallback(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { job.removeCallback(cb); return null; } }) .when(cb) .onResourceReady(anyResource(), isADataSource(), anyBoolean()); job.addCallback(cb, Executors.directExecutor()); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); verify(cb, times(1)).onResourceReady(anyResource(), isADataSource(), anyBoolean()); } @Test public void testRemovingCallbackDuringOnExceptionIsIgnoredIfCallbackHasAlreadyBeenCalled() { harness = new EngineJobHarness(); final EngineJob job = harness.getJob(); final ResourceCallback cb = mockResourceCallback(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { job.removeCallback(cb); return null; } }) .when(cb) .onLoadFailed(any(GlideException.class)); GlideException exception = new GlideException("test"); job.addCallback(cb, Executors.directExecutor()); job.start(harness.decodeJob); job.onLoadFailed(exception); verify(cb, times(1)).onLoadFailed(eq(exception)); } @Test public void testRemovingCallbackDuringOnResourceReadyPreventsCallbackFromBeingCalledIfNotYetCalled() { final EngineJob job = harness.getJob(); final ResourceCallback notYetCalled = mockResourceCallback(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { job.removeCallback(notYetCalled); return null; } }) .when(harness.cb) .onResourceReady(anyResource(), isADataSource(), anyBoolean()); job.addCallback(notYetCalled, Executors.directExecutor()); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); verify(notYetCalled, never()).onResourceReady(anyResource(), isADataSource(), anyBoolean()); } @Test public void testRemovingCallbackDuringOnResourceReadyPreventsResourceFromBeingAcquiredForCallback() { final EngineJob job = harness.getJob(); final ResourceCallback notYetCalled = mockResourceCallback(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { job.removeCallback(notYetCalled); return null; } }) .when(harness.cb) .onResourceReady(anyResource(), isADataSource(), anyBoolean()); job.addCallback(notYetCalled, Executors.directExecutor()); job.start(harness.decodeJob); job.onResourceReady( harness.resource, harness.dataSource, harness.isLoadedFromAlternateCacheKey); // Once for notifying, once for called. verify(harness.engineResource, times(2)).acquire(); } @Test public void testRemovingCallbackDuringOnExceptionPreventsCallbackFromBeingCalledIfNotYetCalled() { harness = new EngineJobHarness(); final EngineJob job = harness.getJob(); final ResourceCallback called = mockResourceCallback(); final ResourceCallback notYetCalled = mockResourceCallback(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { job.removeCallback(notYetCalled); return null; } }) .when(called) .onLoadFailed(any(GlideException.class)); job.addCallback(called, Executors.directExecutor()); job.addCallback(notYetCalled, Executors.directExecutor()); job.start(harness.decodeJob); job.onLoadFailed(new GlideException("test")); verify(notYetCalled, never()).onResourceReady(anyResource(), isADataSource(), anyBoolean()); } @Test public void testCancelsDecodeJobOnCancel() { EngineJob job = harness.getJob(); job.start(harness.decodeJob); job.cancel(); verify(harness.decodeJob).cancel(); } @Test public void testSubmitsDecodeJobToSourceServiceOnSubmitForSource() { EngineJob job = harness.getJob(); harness.diskCacheService.shutdownNow(); job.reschedule(harness.decodeJob); verify(harness.decodeJob).run(); } @Test public void testSubmitsDecodeJobToDiskCacheServiceWhenDecodingFromCacheOnStart() { EngineJob job = harness.getJob(); when(harness.decodeJob.willDecodeFromCache()).thenReturn(true); harness.sourceService.shutdownNow(); job.start(harness.decodeJob); verify(harness.decodeJob).run(); } @Test public void testSubmitsDecodeJobToSourceServiceWhenDecodingFromSourceOnlyOnStart() { EngineJob job = harness.getJob(); when(harness.decodeJob.willDecodeFromCache()).thenReturn(false); harness.diskCacheService.shutdownNow(); job.start(harness.decodeJob); verify(harness.decodeJob).run(); } @Test public void testSubmitsDecodeJobToUnlimitedSourceServiceWhenDecodingFromSourceOnlyOnStart() { harness.useUnlimitedSourceGeneratorPool = true; EngineJob job = harness.getJob(); when(harness.decodeJob.willDecodeFromCache()).thenReturn(false); harness.diskCacheService.shutdownNow(); job.start(harness.decodeJob); verify(harness.decodeJob).run(); } private static ResourceCallback mockResourceCallback() { ResourceCallback result = mock(ResourceCallback.class); when(result.getLock()).thenReturn(result); return result; } @SuppressWarnings("unchecked") private static class MultiCbHarness { final Key key = mock(Key.class); final Resource resource = mockResource(); final EngineResource engineResource = mock(EngineResource.class); final EngineJobListener engineJobListener = mock(EngineJobListener.class); final ResourceListener resourceListener = mock(ResourceListener.class); final boolean isCacheable = true; final boolean useUnlimitedSourceGeneratorPool = false; final boolean useAnimationPool = false; final boolean onlyRetrieveFromCache = false; final int numCbs = 10; final List cbs = new ArrayList<>(); final EngineJob.EngineResourceFactory factory = mock(EngineJob.EngineResourceFactory.class); final EngineJob job; final GlideExecutor diskCacheService = MockGlideExecutor.newMainThreadExecutor(); final GlideExecutor sourceService = MockGlideExecutor.newMainThreadExecutor(); final GlideExecutor sourceUnlimitedService = MockGlideExecutor.newMainThreadExecutor(); final GlideExecutor animationService = MockGlideExecutor.newMainThreadExecutor(); final Pools.Pool> pool = new Pools.SimplePool<>(1); final DecodeJob decodeJob = mock(DecodeJob.class); final DataSource dataSource = DataSource.LOCAL; final boolean isLoadedFromAlternateCacheKey = true; MultiCbHarness() { when(factory.build(resource, isCacheable, key, resourceListener)).thenReturn(engineResource); job = new EngineJob<>( diskCacheService, sourceService, sourceUnlimitedService, animationService, engineJobListener, resourceListener, pool, factory); job.init( key, isCacheable, useUnlimitedSourceGeneratorPool, useAnimationPool, onlyRetrieveFromCache); for (int i = 0; i < numCbs; i++) { cbs.add(mockResourceCallback()); } for (ResourceCallback cb : cbs) { job.addCallback(cb, Executors.directExecutor()); } } } @SuppressWarnings("unchecked") private static class EngineJobHarness { final EngineJob.EngineResourceFactory factory = mock(EngineJob.EngineResourceFactory.class); final Key key = mock(Key.class); final Handler mainHandler = new Handler(); final ResourceCallback cb = mockResourceCallback(); final Resource resource = mockResource(); final EngineResource engineResource = mock(EngineResource.class); final EngineJobListener engineJobListener = mock(EngineJobListener.class); final ResourceListener resourceListener = mock(ResourceListener.class); final GlideExecutor diskCacheService = MockGlideExecutor.newMainThreadExecutor(); final GlideExecutor sourceService = MockGlideExecutor.newMainThreadExecutor(); final GlideExecutor sourceUnlimitedService = MockGlideExecutor.newMainThreadExecutor(); final GlideExecutor animationService = MockGlideExecutor.newMainThreadExecutor(); boolean isCacheable = true; boolean useUnlimitedSourceGeneratorPool = false; final boolean useAnimationPool = false; final boolean onlyRetrieveFromCache = false; final DecodeJob decodeJob = mock(DecodeJob.class); final Pools.Pool> pool = new Pools.SynchronizedPool<>(1); final DataSource dataSource = DataSource.DATA_DISK_CACHE; final boolean isLoadedFromAlternateCacheKey = true; EngineJob getJob() { when(factory.build(resource, isCacheable, key, resourceListener)).thenReturn(engineResource); EngineJob result = new EngineJob<>( diskCacheService, sourceService, sourceUnlimitedService, animationService, engineJobListener, resourceListener, pool, factory); result.init( key, isCacheable, useUnlimitedSourceGeneratorPool, useAnimationPool, onlyRetrieveFromCache); result.addCallback(cb, Executors.directExecutor()); return result; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/EngineKeyTest.java ================================================ package com.bumptech.glide.load.engine; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertThrows; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Option.CacheKeyUpdater; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.signature.ObjectKey; import com.google.common.testing.EqualsTester; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class EngineKeyTest { @Mock private Transformation transformation; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void updateDiskCacheKey_throwsException() throws NoSuchAlgorithmException { // If this test fails, update testEqualsAndHashcode to use KeyTester including regression tests. final EngineKey key = new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, new Options()); assertThrows( UnsupportedOperationException.class, new ThrowingRunnable() { @Override public void run() throws NoSuchAlgorithmException { key.updateDiskCacheKey(MessageDigest.getInstance("SHA-1")); } }); } @Test public void testEqualsAndHashCode() { Options memoryOptions = new Options(); memoryOptions.set(Option.memory("key", new Object()), new Object()); Options diskOptions = new Options(); diskOptions.set( Option.disk( "key", new CacheKeyUpdater() { @Override public void update( @NonNull byte[] keyBytes, @NonNull String value, @NonNull MessageDigest messageDigest) { messageDigest.update(keyBytes); messageDigest.update(value.getBytes(Key.CHARSET)); } }), "value"); new EqualsTester() .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, new Options()), new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, new Options())) .addEqualityGroup( new EngineKey( "otherId", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, new Options())) .addEqualityGroup( new EngineKey( "id", new ObjectKey("otherSignature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, new Options())) .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 200, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, new Options())) .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 100, 200, Collections., Transformation>emptyMap(), Object.class, Object.class, new Options())) .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>singletonMap(Object.class, transformation), Object.class, Object.class, new Options())) .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Integer.class, Object.class, new Options())) .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Integer.class, new Options())) .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, memoryOptions)) .addEqualityGroup( new EngineKey( "id", new ObjectKey("signature"), 100, 100, Collections., Transformation>emptyMap(), Object.class, Object.class, diskOptions)) .testEquals(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/EngineResourceTest.java ================================================ package com.bumptech.glide.load.engine; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.mockResource; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.bumptech.glide.load.Key; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class EngineResourceTest { private EngineResource engineResource; @Mock private EngineResource.ResourceListener listener; @Mock private Key cacheKey; @Mock private Resource resource; @Before public void setUp() { MockitoAnnotations.initMocks(this); engineResource = new EngineResource<>( resource, /* isMemoryCacheable= */ true, /* isRecyclable= */ true, cacheKey, listener); } @Test public void testCanAcquireAndRelease() { engineResource.acquire(); engineResource.release(); verify(listener).onResourceReleased(cacheKey, engineResource); } @Test public void testCanAcquireMultipleTimesAndRelease() { engineResource.acquire(); engineResource.acquire(); engineResource.release(); engineResource.release(); verify(listener).onResourceReleased(eq(cacheKey), eq(engineResource)); } @Test public void testDelegatesGetToWrappedResource() { Object expected = new Object(); when(resource.get()).thenReturn(expected); assertEquals(expected, engineResource.get()); } @Test public void testDelegatesGetSizeToWrappedResource() { int expectedSize = 1234; when(resource.getSize()).thenReturn(expectedSize); assertEquals(expectedSize, engineResource.getSize()); } @Test public void testRecyclesWrappedResourceWhenRecycled() { engineResource.acquire(); engineResource.release(); engineResource.recycle(); verify(resource).recycle(); } @Test(expected = IllegalStateException.class) public void testThrowsIfRecycledTwice() { engineResource.recycle(); engineResource.recycle(); } @Test(expected = IllegalStateException.class) public void testThrowsIfReleasedBeforeAcquired() { engineResource.release(); } @Test(expected = IllegalStateException.class) public void testThrowsIfRecycledWhileAcquired() { engineResource.acquire(); engineResource.recycle(); } @Test(expected = IllegalStateException.class) public void testThrowsIfAcquiredAfterRecycled() { engineResource.recycle(); engineResource.acquire(); } @Test public void testThrowsIfAcquiredOnBackgroundThread() throws InterruptedException { Thread otherThread = new Thread( new Runnable() { @Override public void run() { try { engineResource.acquire(); } catch (IllegalThreadStateException e) { return; } fail("Failed to receive expected IllegalThreadStateException"); } }); otherThread.start(); otherThread.join(); } @Test public void testThrowsIfReleasedOnBackgroundThread() throws InterruptedException { engineResource.acquire(); Thread otherThread = new Thread( new Runnable() { @Override public void run() { try { engineResource.release(); } catch (IllegalThreadStateException e) { return; } fail("Failed to receive expected IllegalThreadStateException"); } }); otherThread.start(); otherThread.join(); } @Test(expected = IllegalStateException.class) public void testThrowsIfReleasedMoreThanAcquired() { engineResource.acquire(); engineResource.release(); engineResource.release(); } @Test(expected = NullPointerException.class) public void testThrowsIfWrappedResourceIsNull() { new EngineResource<>( /* toWrap= */ null, /* isMemoryCacheable= */ false, /* isRecyclable= */ true, cacheKey, listener); } @Test public void testCanSetAndGetIsCacheable() { engineResource = new EngineResource<>( mockResource(), /* isMemoryCacheable= */ true, /* isRecyclable= */ true, cacheKey, listener); assertTrue(engineResource.isMemoryCacheable()); engineResource = new EngineResource<>( mockResource(), /* isMemoryCacheable= */ false, /* isRecyclable= */ true, cacheKey, listener); assertFalse(engineResource.isMemoryCacheable()); } @Test public void release_whenNotRecycleable_doesNotRecycleResource() { resource = mockResource(); engineResource = new EngineResource<>( resource, /* isMemoryCacheable= */ true, /* isRecyclable= */ false, cacheKey, listener); engineResource.recycle(); verify(listener, never()).onResourceReleased(any(Key.class), any(EngineResource.class)); verify(resource, never()).recycle(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/EngineTest.java ================================================ package com.bumptech.glide.load.engine; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.anyResource; import static com.bumptech.glide.tests.Util.isADataSource; import static com.bumptech.glide.tests.Util.mockResource; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.bumptech.glide.GlideContext; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.LruResourceCache; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.load.engine.executor.GlideExecutor; import com.bumptech.glide.load.engine.executor.MockGlideExecutor; import com.bumptech.glide.request.ResourceCallback; import com.bumptech.glide.tests.BackgroundUtil; import com.bumptech.glide.util.Executors; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) @SuppressWarnings("unchecked") public class EngineTest { private EngineTestHarness harness; @Before public void setUp() { harness = new EngineTestHarness(); } @Test public void testNewRunnerIsCreatedAndPostedWithNoExistingLoad() { harness.doLoad(); verify(harness.job).start((DecodeJob) any()); } @Test public void testCallbackIsAddedToNewEngineJobWithNoExistingLoad() { harness.doLoad(); verify(harness.job).addCallback(eq(harness.cb), any(Executor.class)); } @Test public void testLoadStatusIsReturnedForNewLoad() { assertNotNull(harness.doLoad()); } @Test public void testEngineJobReceivesRemoveCallbackFromLoadStatus() { Engine.LoadStatus loadStatus = harness.doLoad(); loadStatus.cancel(); verify(harness.job).removeCallback(eq(harness.cb)); } @Test public void testNewRunnerIsAddedToRunnersMap() { harness.doLoad(); assertThat(harness.jobs.getAll()).containsKey(harness.cacheKey); } @Test public void testNewRunnerIsNotCreatedAndPostedWithExistingLoad() { harness.doLoad(); harness.doLoad(); verify(harness.job, times(1)).start((DecodeJob) any()); } @Test public void testCallbackIsAddedToExistingRunnerWithExistingLoad() { harness.doLoad(); ResourceCallback newCallback = mock(ResourceCallback.class); harness.cb = newCallback; harness.doLoad(); verify(harness.job).addCallback(eq(newCallback), any(Executor.class)); } @Test public void testLoadStatusIsReturnedForExistingJob() { harness.doLoad(); Engine.LoadStatus loadStatus = harness.doLoad(); assertNotNull(loadStatus); } @Test public void testResourceIsReturnedFromActiveResourcesIfPresent() { harness.activeResources.activate(harness.cacheKey, harness.resource); harness.doLoad(); verify(harness.cb) .onResourceReady(eq(harness.resource), eq(DataSource.MEMORY_CACHE), eq(false)); } @Test public void testResourceIsAcquiredIfReturnedFromActiveResources() { harness.activeResources.activate(harness.cacheKey, harness.resource); harness.doLoad(); verify(harness.resource).acquire(); } @Test public void testNewLoadIsNotStartedIfResourceIsActive() { harness.activeResources.activate(harness.cacheKey, harness.resource); harness.doLoad(); verify(harness.job, never()).start(anyDecodeJobOrNull()); } @Test public void testNullLoadStatusIsReturnedIfResourceIsActive() { harness.activeResources.activate(harness.cacheKey, harness.resource); assertNull(harness.doLoad()); } @Test public void load_withResourceInActiveResources_doesNotCheckMemoryCache() { harness.activeResources.activate(harness.cacheKey, harness.resource); harness.doLoad(); verify(harness.cb) .onResourceReady(eq(harness.resource), eq(DataSource.MEMORY_CACHE), eq(false)); verify(harness.cache, never()).remove(any(Key.class)); } @Test public void testActiveResourcesIsNotCheckedIfNotMemoryCacheable() { harness.activeResources.activate(harness.cacheKey, harness.resource); harness.isMemoryCacheable = false; harness.doLoad(); verify(harness.resource, never()).acquire(); verify(harness.job).start((DecodeJob) any()); } @Test public void testCacheIsCheckedIfMemoryCacheable() { when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(harness.resource); harness.doLoad(); verify(harness.cb) .onResourceReady(eq(harness.resource), eq(DataSource.MEMORY_CACHE), eq(false)); } @Test public void testCacheIsNotCheckedIfNotMemoryCacheable() { when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(harness.resource); harness.isMemoryCacheable = false; harness.doLoad(); verify(harness.job).start((DecodeJob) any()); } @Test public void testResourceIsReturnedFromCacheIfPresent() { when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(harness.resource); harness.doLoad(); verify(harness.cb) .onResourceReady(eq(harness.resource), eq(DataSource.MEMORY_CACHE), eq(false)); } @Test public void testHandlesNonEngineResourcesFromCacheIfPresent() { final Object expected = new Object(); @SuppressWarnings("rawtypes") Resource fromCache = mockResource(); when(fromCache.get()).thenReturn(expected); when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(fromCache); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { Resource resource = (Resource) invocationOnMock.getArguments()[0]; assertEquals(expected, resource.get()); return null; } }) .when(harness.cb) .onResourceReady(anyResource(), isADataSource(), anyBoolean()); harness.doLoad(); verify(harness.cb).onResourceReady(anyResource(), isADataSource(), anyBoolean()); } @Test public void testResourceIsAddedToActiveResourceIfReturnedFromCache() { when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(harness.resource); harness.doLoad(); EngineResource activeResource = harness.activeResources.get(harness.cacheKey); assertThat(activeResource).isEqualTo(harness.resource); } @Test public void testResourceIsAcquiredIfReturnedFromCache() { when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(harness.resource); harness.doLoad(); verify(harness.resource).acquire(); } @Test public void testNewLoadIsNotStartedIfResourceIsCached() { when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(harness.resource); harness.doLoad(); verify(harness.job, never()).start(anyDecodeJobOrNull()); } @Test public void testNullLoadStatusIsReturnedForCachedResource() { when(harness.cache.remove(eq(harness.cacheKey))).thenReturn(harness.resource); Engine.LoadStatus loadStatus = harness.doLoad(); assertNull(loadStatus); } @Test public void testRunnerIsRemovedFromRunnersOnEngineNotifiedJobComplete() { harness.doLoad(); harness.callOnEngineJobComplete(); assertThat(harness.jobs.getAll()).doesNotContainKey(harness.cacheKey); } @Test public void testEngineIsNotSetAsResourceListenerIfResourceIsNullOnJobComplete() { harness.doLoad(); harness.getEngine().onEngineJobComplete(harness.job, harness.cacheKey, /* resource= */ null); } @Test public void testResourceIsAddedToActiveResourcesOnEngineComplete() { when(harness.resource.isMemoryCacheable()).thenReturn(true); harness.callOnEngineJobComplete(); EngineResource resource = harness.activeResources.get(harness.cacheKey); assertThat(harness.resource).isEqualTo(resource); } @Test public void testDoesNotPutNullResourceInActiveResourcesOnEngineComplete() { harness.getEngine().onEngineJobComplete(harness.job, harness.cacheKey, /* resource= */ null); assertThat(harness.activeResources.get(harness.cacheKey)).isNull(); } @Test public void testDoesNotPutResourceThatIsNotCacheableInActiveResourcesOnEngineComplete() { when(harness.resource.isMemoryCacheable()).thenReturn(false); harness.callOnEngineJobComplete(); assertThat(harness.activeResources.get(harness.cacheKey)).isNull(); } @Test public void testRunnerIsRemovedFromRunnersOnEngineNotifiedJobCancel() { harness.doLoad(); harness.getEngine().onEngineJobCancelled(harness.job, harness.cacheKey); assertThat(harness.jobs.getAll()).doesNotContainKey(harness.cacheKey); } @Test public void testJobIsNotRemovedFromJobsIfOldJobIsCancelled() { harness.doLoad(); harness.getEngine().onEngineJobCancelled(mock(EngineJob.class), harness.cacheKey); assertEquals(harness.job, harness.jobs.get(harness.cacheKey, harness.onlyRetrieveFromCache)); } @Test public void testResourceIsAddedToCacheOnReleased() { final Object expected = new Object(); when(harness.resource.isMemoryCacheable()).thenReturn(true); when(harness.resource.get()).thenReturn(expected); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { Resource resource = (Resource) invocationOnMock.getArguments()[1]; assertEquals(expected, resource.get()); return null; } }) .when(harness.cache) .put(eq(harness.cacheKey), anyResource()); harness.getEngine().onResourceReleased(harness.cacheKey, harness.resource); verify(harness.cache).put(eq(harness.cacheKey), anyResource()); } @Test public void testResourceIsNotAddedToCacheOnReleasedIfNotCacheable() { when(harness.resource.isMemoryCacheable()).thenReturn(false); harness.getEngine().onResourceReleased(harness.cacheKey, harness.resource); verify(harness.cache, never()).put(eq(harness.cacheKey), eq(harness.resource)); } @Test public void testResourceIsRecycledIfNotCacheableWhenReleased() { when(harness.resource.isMemoryCacheable()).thenReturn(false); harness.getEngine().onResourceReleased(harness.cacheKey, harness.resource); verify(harness.resourceRecycler).recycle(eq(harness.resource), eq(false)); } @Test public void testResourceIsRemovedFromActiveResourcesWhenReleased() { harness.activeResources.activate(harness.cacheKey, harness.resource); harness.getEngine().onResourceReleased(harness.cacheKey, harness.resource); assertThat(harness.activeResources.get(harness.cacheKey)).isNull(); } @Test public void testEngineAddedAsListenerToMemoryCache() { harness.getEngine(); verify(harness.cache).setResourceRemovedListener(eq(harness.getEngine())); } @Test public void testResourceIsRecycledWhenRemovedFromCache() { harness.getEngine().onResourceRemoved(harness.resource); verify(harness.resourceRecycler).recycle(eq(harness.resource), eq(true)); } @Test public void testJobIsPutInJobWithCacheKeyWithRelevantIds() { harness.doLoad(); assertThat(harness.jobs.getAll()).containsEntry(harness.cacheKey, harness.job); } @Test public void testKeyFactoryIsGivenNecessaryArguments() { harness.doLoad(); verify(harness.keyFactory) .buildKey( eq(harness.model), eq(harness.signature), eq(harness.width), eq(harness.height), eq(harness.transformations), eq(Object.class), eq(Object.class), eq(harness.options)); } @Test public void testFactoryIsGivenNecessaryArguments() { harness.doLoad(); verify(harness.engineJobFactory) .build( eq(harness.cacheKey), eq(true) /*isMemoryCacheable*/, eq(false) /*useUnlimitedSourceGeneratorPool*/, /* useAnimationPool= */ eq(false), /* onlyRetrieveFromCache= */ eq(false)); } @Test public void testFactoryIsGivenNecessaryArgumentsWithUnlimitedPool() { harness.useUnlimitedSourceGeneratorPool = true; harness.doLoad(); verify(harness.engineJobFactory) .build( eq(harness.cacheKey), eq(true) /*isMemoryCacheable*/, eq(true) /*useUnlimitedSourceGeneratorPool*/, /* useAnimationPool= */ eq(false), /* onlyRetrieveFromCache= */ eq(false)); } @Test public void testReleaseReleasesEngineResource() { EngineResource engineResource = mock(EngineResource.class); harness.getEngine().release(engineResource); verify(engineResource).release(); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfAskedToReleaseNonEngineResource() { harness.getEngine().release(mockResource()); } @Test public void load_whenCalledOnBackgroundThread_doesNotThrow() throws InterruptedException { BackgroundUtil.testInBackground( new BackgroundUtil.BackgroundTester() { @Override public void runTest() { harness.doLoad(); } }); } @Test public void load_afterResourceIsLoadedInActiveResources_returnsFromMemoryCache() { when(harness.resource.isMemoryCacheable()).thenReturn(true); doAnswer( new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) { harness.callOnEngineJobComplete(); return null; } }) .when(harness.job) .start(anyDecodeJobOrNull()); harness.doLoad(); harness.doLoad(); verify(harness.cb).onResourceReady(any(Resource.class), eq(DataSource.MEMORY_CACHE), eq(false)); } @Test public void load_afterResourceIsLoadedAndReleased_returnsFromMemoryCache() { harness.cache = new LruResourceCache(100); when(harness.resource.isMemoryCacheable()).thenReturn(true); doAnswer( new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) { harness.callOnEngineJobComplete(); return null; } }) .when(harness.job) .start(anyDecodeJobOrNull()); harness.doLoad(); harness.getEngine().onResourceReleased(harness.cacheKey, harness.resource); harness.doLoad(); verify(harness.cb).onResourceReady(any(Resource.class), eq(DataSource.MEMORY_CACHE), eq(false)); } @Test public void load_withOnlyRetrieveFromCache_andPreviousNormalLoad_startsNewLoad() { EngineJob first = harness.job; harness.doLoad(); EngineJob second = mock(EngineJob.class); harness.job = second; harness.onlyRetrieveFromCache = true; harness.doLoad(); verify(first).start(anyDecodeJobOrNull()); verify(second).start(anyDecodeJobOrNull()); } @Test public void load_withNormalLoad_afterPreviousRetrieveFromCache_startsNewLoad() { EngineJob first = harness.job; harness.onlyRetrieveFromCache = true; harness.doLoad(); EngineJob second = mock(EngineJob.class); harness.job = second; harness.onlyRetrieveFromCache = false; harness.doLoad(); verify(first).start(anyDecodeJobOrNull()); verify(second).start(anyDecodeJobOrNull()); } @Test public void load_afterFinishedOnlyRetrieveFromCache_withPendingNormal_doesNotStartNewLoad() { EngineJob firstNormal = harness.job; harness.doLoad(); harness.job = mock(EngineJob.class); harness.onlyRetrieveFromCache = true; harness.doLoad(); harness.callOnEngineJobComplete(); EngineJob secondNormal = mock(EngineJob.class); harness.job = secondNormal; harness.onlyRetrieveFromCache = false; harness.doLoad(); verify(firstNormal).start(anyDecodeJobOrNull()); verify(secondNormal, never()).start(anyDecodeJobOrNull()); } @Test public void load_afterCancelledOnlyRetrieveFromCache_withPendingNormal_doesNotStartNewLoad() { EngineJob firstNormal = harness.job; harness.doLoad(); harness.job = mock(EngineJob.class); harness.onlyRetrieveFromCache = true; harness.doLoad(); harness.getEngine().onEngineJobCancelled(harness.job, harness.cacheKey); EngineJob secondNormal = mock(EngineJob.class); harness.job = secondNormal; harness.onlyRetrieveFromCache = false; harness.doLoad(); verify(firstNormal).start(anyDecodeJobOrNull()); verify(secondNormal, never()).start(anyDecodeJobOrNull()); } @Test public void load_withOnlyRetrieveFromCache_withOtherRetrieveFromCachePending_doesNotStartNew() { harness.onlyRetrieveFromCache = true; harness.doLoad(); EngineJob second = mock(EngineJob.class); harness.job = second; harness.doLoad(); verify(second, never()).start(anyDecodeJobOrNull()); } @Test public void load_withOnlyRetrieveFromCache_afterPreviousFinishedOnlyFromCacheLoad_startsNew() { harness.onlyRetrieveFromCache = true; harness.doLoad(); harness.callOnEngineJobComplete(); EngineJob second = mock(EngineJob.class); harness.job = second; harness.doLoad(); verify(second).start(anyDecodeJobOrNull()); } @Test public void load_withOnlyRetrieveFromCache_afterPreviousCancelledOnlyFromCacheLoad_startsNew() { harness.onlyRetrieveFromCache = true; harness.doLoad(); harness.getEngine().onEngineJobCancelled(harness.job, harness.cacheKey); EngineJob second = mock(EngineJob.class); harness.job = second; harness.doLoad(); verify(second).start(anyDecodeJobOrNull()); } @Test public void onEngineJobComplete_withOldJobForKey_doesNotRemoveJob() { harness.doLoad(); harness .getEngine() .onEngineJobComplete(mock(EngineJob.class), harness.cacheKey, harness.resource); harness.job = mock(EngineJob.class); harness.doLoad(); verify(harness.job, never()).start(anyDecodeJobOrNull()); } @Test public void onEngineJobCancelled_withOldJobForKey_doesNotRemoveJob() { harness.doLoad(); harness.getEngine().onEngineJobCancelled(mock(EngineJob.class), harness.cacheKey); harness.job = mock(EngineJob.class); harness.doLoad(); verify(harness.job, never()).start(anyDecodeJobOrNull()); } @Test public void onEngineJobComplete_withOnlyRetrieveFromCacheAndOldJobForKey_doesNotRemoveJob() { harness.onlyRetrieveFromCache = true; harness.doLoad(); harness .getEngine() .onEngineJobComplete(mock(EngineJob.class), harness.cacheKey, harness.resource); harness.job = mock(EngineJob.class); harness.doLoad(); verify(harness.job, never()).start(anyDecodeJobOrNull()); } @Test public void onEngineJobCancelled_withOnlyRetrieveFromCacheAndOldJobForKey_doesNotRemoveJob() { harness.onlyRetrieveFromCache = true; harness.doLoad(); harness.getEngine().onEngineJobCancelled(mock(EngineJob.class), harness.cacheKey); harness.job = mock(EngineJob.class); harness.doLoad(); verify(harness.job, never()).start(anyDecodeJobOrNull()); } @SuppressWarnings({"unchecked", "rawtypes"}) private static DecodeJob anyDecodeJobOrNull() { return any(); } private static class EngineTestHarness { final EngineKey cacheKey = mock(EngineKey.class); final EngineKeyFactory keyFactory = mock(EngineKeyFactory.class); ResourceCallback cb = mock(ResourceCallback.class); @SuppressWarnings("rawtypes") final EngineResource resource = mock(EngineResource.class); final Jobs jobs = new Jobs(); final ActiveResources activeResources = new ActiveResources(/* isActiveResourceRetentionAllowed= */ true); final int width = 100; final int height = 100; final Object model = new Object(); MemoryCache cache = mock(MemoryCache.class); EngineJob job; private Engine engine; final Engine.EngineJobFactory engineJobFactory = mock(Engine.EngineJobFactory.class); final Engine.DecodeJobFactory decodeJobFactory = mock(Engine.DecodeJobFactory.class); final ResourceRecycler resourceRecycler = mock(ResourceRecycler.class); final Key signature = mock(Key.class); final Map, Transformation> transformations = new HashMap<>(); final Options options = new Options(); final GlideContext glideContext = mock(GlideContext.class); boolean isMemoryCacheable = true; boolean useUnlimitedSourceGeneratorPool = false; boolean onlyRetrieveFromCache = false; final boolean isScaleOnlyOrNoTransform = true; EngineTestHarness() { when(keyFactory.buildKey( eq(model), eq(signature), anyInt(), anyInt(), eq(transformations), eq(Object.class), eq(Object.class), eq(options))) .thenReturn(cacheKey); when(resource.getResource()).thenReturn(mock(Resource.class)); job = mock(EngineJob.class); } void callOnEngineJobComplete() { getEngine().onEngineJobComplete(job, cacheKey, resource); } Engine.LoadStatus doLoad() { when(engineJobFactory.build( eq(cacheKey), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean())) .thenReturn((EngineJob) job); when(job.onlyRetrieveFromCache()).thenReturn(onlyRetrieveFromCache); return getEngine() .load( glideContext, model, signature, width, height, Object.class /*resourceClass*/, Object.class /*transcodeClass*/, Priority.HIGH, DiskCacheStrategy.ALL, transformations, false /*isTransformationRequired*/, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceGeneratorPool, /* useAnimationPool= */ false, onlyRetrieveFromCache, cb, Executors.directExecutor()); } Engine getEngine() { if (engine == null) { engine = new Engine( cache, mock(DiskCache.Factory.class), GlideExecutor.newDiskCacheExecutor(), MockGlideExecutor.newMainThreadExecutor(), MockGlideExecutor.newMainThreadExecutor(), MockGlideExecutor.newMainThreadExecutor(), jobs, keyFactory, activeResources, engineJobFactory, decodeJobFactory, resourceRecycler, /* isActiveResourceRetentionAllowed= */ true); } return engine; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/ResourceCacheKeyTest.java ================================================ package com.bumptech.glide.load.engine; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Option.CacheKeyUpdater; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.util.Arrays; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class ResourceCacheKeyTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Transformation transformation1; @Mock private Transformation transformation2; private LruArrayPool arrayPool; @Before public void setUp() { MockitoAnnotations.initMocks(this); arrayPool = new LruArrayPool(); doAnswer(new Util.WriteDigest("transformation1")) .when(transformation1) .updateDiskCacheKey(any(MessageDigest.class)); doAnswer(new Util.WriteDigest("transformation1")) .when(transformation2) .updateDiskCacheKey(any(MessageDigest.class)); } @Test public void testEqualsAndHashCode() { Options memoryOptions = new Options(); memoryOptions.set(Option.memory("key", new Object()), new Object()); Options diskOptions = new Options(); diskOptions.set( Option.disk( "key", new CacheKeyUpdater() { @Override public void update( @NonNull byte[] keyBytes, @NonNull String value, @NonNull MessageDigest messageDigest) { messageDigest.update(keyBytes); messageDigest.update(value.getBytes(Key.CHARSET)); } }), "value"); for (int i = 0; i < 20; i++) { byte[] array = new byte[9]; Arrays.fill(array, (byte) 2); arrayPool.put(array); } keyTester .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation1, Object.class, new Options()), new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation1, Object.class, new Options())) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("otherSource"), new ObjectKey("signature"), 100, 100, transformation1, Object.class, new Options())) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("otherSignature"), 100, 100, transformation1, Object.class, new Options())) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 200, 100, transformation1, Object.class, new Options())) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 200, transformation1, Object.class, new Options())) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation2, Object.class, new Options())) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation1, Integer.class, new Options())) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation1, Object.class, memoryOptions)) .addEquivalenceGroup( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation1, Object.class, diskOptions)) .addRegressionTest( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation1, Object.class, new Options()), "04d632bfe8e588544909fc44edb7328fa28bea6831b96927ade22b44818654e2") .addRegressionTest( new ResourceCacheKey( arrayPool, new ObjectKey("source"), new ObjectKey("signature"), 100, 100, transformation1, Object.class, diskOptions), "781ff8cd30aaaf248134580004ea6d63a1b87ae20ea0f769caf379d7d84986d0") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/ResourceRecyclerTest.java ================================================ package com.bumptech.glide.load.engine; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.mockResource; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.os.Looper; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ResourceRecyclerTest { private ResourceRecycler recycler; @Before public void setUp() { recycler = new ResourceRecycler(); } @Test public void recycle_withoutForceNextFrame_recyclesResourceSynchronously() { Resource resource = mockResource(); Shadows.shadowOf(Looper.getMainLooper()).pause(); recycler.recycle(resource, /* forceNextFrame= */ false); verify(resource).recycle(); } @Test public void recycle_withForceNextFrame_postsRecycle() { Resource resource = mockResource(); Shadows.shadowOf(Looper.getMainLooper()).pause(); recycler.recycle(resource, /* forceNextFrame= */ true); verify(resource, never()).recycle(); Shadows.shadowOf(Looper.getMainLooper()).runToEndOfTasks(); verify(resource).recycle(); } @Test public void testDoesNotRecycleChildResourceSynchronously() { Resource parent = mockResource(); final Resource child = mockResource(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { recycler.recycle(child, /* forceNextFrame= */ false); return null; } }) .when(parent) .recycle(); Shadows.shadowOf(Looper.getMainLooper()).pause(); recycler.recycle(parent, /* forceNextFrame= */ false); verify(parent).recycle(); verify(child, never()).recycle(); Shadows.shadowOf(Looper.getMainLooper()).runOneTask(); verify(child).recycle(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/bitmap_recycle/AttributeStrategyKeyTest.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.graphics.Bitmap; import com.bumptech.glide.load.engine.bitmap_recycle.AttributeStrategy.Key; import com.google.common.testing.EqualsTester; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class AttributeStrategyKeyTest { private AttributeStrategy.KeyPool keyPool; @Before public void setUp() { keyPool = mock(AttributeStrategy.KeyPool.class); } @Test public void testEquality() { Key first = new Key(keyPool); first.init(100, 100, Bitmap.Config.ARGB_4444); Key second = new Key(keyPool); second.init(100, 100, Bitmap.Config.ARGB_4444); Key third = new Key(keyPool); third.init(200, 100, Bitmap.Config.ARGB_4444); Key fourth = new Key(keyPool); fourth.init(100, 200, Bitmap.Config.ARGB_4444); Key fifth = new Key(keyPool); fifth.init(100, 100, Bitmap.Config.RGB_565); new EqualsTester() .addEqualityGroup(first, second) .addEqualityGroup(third) .addEqualityGroup(fourth) .addEqualityGroup(fifth) .testEquals(); } @Test public void testReturnsSelfToPoolOnOffer() { Key key = new Key(keyPool); key.offer(); verify(keyPool).offer(eq(key)); } @Test public void testInitSetsAttributes() { Key key = new Key(keyPool); key.init(100, 100, Bitmap.Config.ARGB_4444); Key other = new Key(keyPool); other.init(200, 200, Bitmap.Config.RGB_565); assertNotEquals(key, other); key.init(200, 200, Bitmap.Config.RGB_565); assertEquals(key, other); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/bitmap_recycle/AttributeStrategyTest.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import android.graphics.Bitmap; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class AttributeStrategyTest { private AttributeStrategy strategy; @Before public void setUp() throws Exception { strategy = new AttributeStrategy(); } @Test public void testIGetNullIfNoMatchingBitmapExists() { assertNull(strategy.get(100, 100, Bitmap.Config.ARGB_8888)); } @Test public void testICanAddAndGetABitmapOfTheSameSizeAndDimensions() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); strategy.put(bitmap); assertEquals( bitmap, strategy.get(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888)); } @Test public void testICantGetABitmapOfTheSameDimensionsButDifferentConfigs() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); strategy.put(bitmap); assertNull(strategy.get(100, 100, Bitmap.Config.RGB_565)); } @Test public void testICantGetABitmapOfTheSameDimensionsAndSizeButDifferentConfigs() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); strategy.put(bitmap); assertNull(strategy.get(100, 100, Bitmap.Config.RGB_565)); } @Test public void testICantGetABitmapOfDifferentWidths() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); strategy.put(bitmap); assertNull(strategy.get(99, 100, Bitmap.Config.ARGB_8888)); } @Test public void testICantGetABitmapOfDifferentHeights() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); strategy.put(bitmap); assertNull(strategy.get(100, 99, Bitmap.Config.ARGB_8888)); } @Test public void testICantGetABitmapOfDifferentDimensionsButTheSameSize() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); strategy.put(bitmap); assertNull(strategy.get(50, 200, Bitmap.Config.ARGB_8888)); } @Test public void testMultipleBitmapsOfDifferentAttributesCanBeAddedAtOnce() { Bitmap first = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); Bitmap second = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Bitmap third = Bitmap.createBitmap(120, 120, Bitmap.Config.RGB_565); strategy.put(first); strategy.put(second); strategy.put(third); assertEquals(first, strategy.get(100, 100, Bitmap.Config.RGB_565)); assertEquals(second, strategy.get(100, 100, Bitmap.Config.ARGB_8888)); assertEquals(third, strategy.get(120, 120, Bitmap.Config.RGB_565)); } @Test public void testLeastRecentlyUsedAttributeSetIsRemovedFirst() { final Bitmap leastRecentlyUsed = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8); final Bitmap other = Bitmap.createBitmap(1000, 1000, Bitmap.Config.RGB_565); final Bitmap mostRecentlyUsed = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); strategy.get(100, 100, Bitmap.Config.ALPHA_8); strategy.get(1000, 1000, Bitmap.Config.RGB_565); strategy.get(100, 100, Bitmap.Config.ARGB_8888); strategy.put(other); strategy.put(leastRecentlyUsed); strategy.put(mostRecentlyUsed); Bitmap removed = strategy.removeLast(); assertEquals( "Expected=" + strategy.logBitmap(leastRecentlyUsed) + " got=" + strategy.logBitmap(removed), leastRecentlyUsed, removed); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/bitmap_recycle/GroupedLinkedMapTest.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GroupedLinkedMapTest { private GroupedLinkedMap map; @Before public void setUp() { map = new GroupedLinkedMap<>(); } @Test public void testReturnsNullForGetWithNoBitmap() { Key key = new Key("key", /* width= */ 1, /* height= */ 1); assertNull(map.get(key)); } @Test public void testCanAddAndRemoveABitmap() { Key key = new Key("key", 1, 1); Object expected = new Object(); map.put(key, expected); assertThat(map.get(key)).isEqualTo(expected); } @Test public void testCanAddAndRemoveMoreThanOneBitmapForAGivenKey() { Key key = new Key("key", 1, 1); Integer value = 20; int numToAdd = 10; for (int i = 0; i < numToAdd; i++) { map.put(key, value); } for (int i = 0; i < numToAdd; i++) { assertThat(map.get(key)).isEqualTo(value); } } @Test public void testLeastRecentlyRetrievedKeyIsLeastRecentlyUsed() { Key firstKey = new Key("key", 1, 1); Integer firstValue = 10; map.put(firstKey, firstValue); map.put(firstKey, firstValue); Key secondKey = new Key("key", 2, 2); Integer secondValue = 20; map.put(secondKey, secondValue); map.get(firstKey); assertThat(map.removeLast()).isEqualTo(secondValue); } @Test public void testAddingAnEntryDoesNotMakeItMostRecentlyUsed() { Key firstKey = new Key("key", 1, 1); Integer firstValue = 10; map.put(firstKey, firstValue); map.put(firstKey, firstValue); map.get(firstKey); Integer secondValue = 20; map.put(new Key("key", 2, 2), secondValue); assertThat(map.removeLast()).isEqualTo(secondValue); } private static final class Key implements Poolable { private final String key; private final int width; private final int height; Key(String key, int width, int height) { this.key = key; this.width = width; this.height = height; } @Override public boolean equals(Object o) { if (o instanceof Key) { Key other = (Key) o; return key.equals(other.key) && width == other.width && height == other.height; } return false; } @Override public int hashCode() { int result = key != null ? key.hashCode() : 0; result = 31 * result + width; result = 31 * result + height; return result; } @Override public void offer() { // Do nothing. } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/bitmap_recycle/LruArrayPoolTest.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import static android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE; import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL; import static android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class LruArrayPoolTest { private static final int MAX_SIZE = 10; private static final int MAX_PUT_SIZE = MAX_SIZE / 2; private static final Class ARRAY_CLASS = byte[].class; private static final ArrayAdapterInterface ADAPTER = new ByteArrayAdapter(); private LruArrayPool pool; @Before public void setUp() { pool = new LruArrayPool(MAX_SIZE); } @Test public void testNewPoolIsEmpty() { assertEquals(pool.getCurrentSize(), 0); } @Test public void testICanAddAndGetValidArray() { int size = 758; int value = 564; fillPool(pool, size - 1, value); pool.put(createArray(ARRAY_CLASS, size, value)); Object array = pool.get(size, ARRAY_CLASS); assertNotNull(array); assertTrue(array.getClass() == ARRAY_CLASS); assertTrue(ADAPTER.getArrayLength((byte[]) array) >= size); assertTrue(((byte[]) array)[0] == (byte) 0); } @Test public void testItIsSizeLimited() { fillPool(pool, MAX_SIZE / ADAPTER.getElementSizeInBytes() + 1, 1); assertTrue(pool.getCurrentSize() <= MAX_SIZE); } @Test public void testArrayLargerThanPoolIsNotAdded() { pool = new LruArrayPool(MAX_SIZE); pool.put(createArray(ARRAY_CLASS, MAX_SIZE / ADAPTER.getElementSizeInBytes() + 1, 0)); assertEquals(0, pool.getCurrentSize()); } @Test public void testClearMemoryRemovesAllArrays() { fillPool(pool, MAX_SIZE / ADAPTER.getElementSizeInBytes() + 1, 0); pool.clearMemory(); assertEquals(0, pool.getCurrentSize()); } @Test public void testTrimMemoryUiHiddenOrLessRemovesHalfOfArrays() { testTrimMemory(MAX_SIZE, TRIM_MEMORY_UI_HIDDEN, MAX_SIZE / 2); } @Test public void testTrimMemoryRunningCriticalRemovesHalfOfBitmaps() { testTrimMemory(MAX_SIZE, TRIM_MEMORY_RUNNING_CRITICAL, MAX_SIZE / 2); } @Test public void testTrimMemoryUiHiddenOrLessRemovesNoArraysIfPoolLessThanHalfFull() { testTrimMemory(MAX_SIZE / 2, TRIM_MEMORY_UI_HIDDEN, MAX_SIZE / 2); } @Test public void testTrimMemoryBackgroundOrGreaterRemovesAllArrays() { for (int trimLevel : new int[] {TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_COMPLETE}) { testTrimMemory(MAX_SIZE, trimLevel, 0); } } @Test public void get_withEmptyPool_returnsExactArray() { assertThat(pool.get(MAX_PUT_SIZE, byte[].class)).hasLength(MAX_PUT_SIZE); } @Test public void get_withPoolContainingLargerArray_returnsLargerArray() { byte[] expected = new byte[MAX_PUT_SIZE]; pool.put(expected); assertThat(pool.get(MAX_PUT_SIZE - 1, byte[].class)).isSameInstanceAs(expected); } @Test public void get_withPoolContainingSmallerArray_returnsExactArray() { pool.put(new byte[MAX_PUT_SIZE - 1]); assertThat(pool.get(MAX_PUT_SIZE, byte[].class)).hasLength(MAX_PUT_SIZE); } @Test public void get_withPoolLessThanHalfFull_returnsFromPools() { int size = MAX_SIZE / 2; byte[] expected = new byte[size]; pool.put(expected); assertThat(pool.get(1, byte[].class)).isSameInstanceAs(expected); } @Test public void get_withPoolMoreThanHalfFull_sizeMoreThanHalfArrayInPool_returnsArray() { Set expected = new HashSet<>(); for (int i = 0; i < 3; i++) { byte[] toPut = new byte[MAX_SIZE / 3]; expected.add(toPut); pool.put(toPut); } byte[] received = pool.get(2, byte[].class); assertThat(expected).contains(received); } @Test public void get_withPoolMoreThanHalfFull_sizeLessThanHalfArrayInPool_returnsNewArray() { pool = new LruArrayPool(100); for (int i = 0; i < 3; i++) { byte[] toPut = new byte[100 / 3]; pool.put(toPut); } int requestedSize = 100 / 3 / LruArrayPool.MAX_OVER_SIZE_MULTIPLE; byte[] received = pool.get(requestedSize, byte[].class); assertThat(received).hasLength(requestedSize); } @Test public void getExact_withEmptyPool_returnsExactArray() { byte[] result = pool.getExact(MAX_PUT_SIZE, byte[].class); assertThat(result).hasLength(MAX_PUT_SIZE); } @Test public void getExact_withPoolContainingLargerArray_returnsExactArray() { pool.put(new byte[MAX_PUT_SIZE]); int expectedSize = MAX_PUT_SIZE - 1; assertThat(pool.getExact(expectedSize, byte[].class)).hasLength(expectedSize); } @Test public void getExact_withPoolContainingSmallerArray_returnsExactArray() { pool.put(new byte[MAX_PUT_SIZE - 1]); assertThat(pool.getExact(MAX_PUT_SIZE, byte[].class)).hasLength(MAX_PUT_SIZE); } @Test public void getExact_withPoolContainingExactArray_returnsArray() { byte[] expected = new byte[MAX_PUT_SIZE]; pool.put(expected); assertThat(pool.getExact(MAX_PUT_SIZE, byte[].class)).isSameInstanceAs(expected); } @Test public void put_withArrayMoreThanHalfPoolSize_doesNotRetainArray() { int targetSize = (MAX_SIZE / 2) + 1; byte[] toPut = new byte[targetSize]; pool.put(toPut); assertThat(pool.getCurrentSize()).isEqualTo(0); assertThat(pool.get(targetSize, byte[].class)).isNotSameInstanceAs(toPut); } private void testTrimMemory(int fillSize, int trimLevel, int expectedSize) { pool = new LruArrayPool(MAX_SIZE); fillPool(pool, fillSize / ADAPTER.getElementSizeInBytes(), 1); pool.trimMemory(trimLevel); assertEquals("Failed level=" + trimLevel, expectedSize, pool.getCurrentSize()); } private void fillPool(LruArrayPool pool, int arrayCount, int arrayLength) { for (int i = 0; i < arrayCount; i++) { pool.put(createArray(ARRAY_CLASS, arrayLength, 10)); } } @SuppressWarnings("unchecked") private static T createArray(Class type, int size, int value) { Object array = null; if (type.equals(int[].class)) { array = new int[size]; Arrays.fill((int[]) array, value); } else if (type.equals(byte[].class)) { array = new byte[size]; Arrays.fill((byte[]) array, (byte) value); } return (T) array; } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/bitmap_recycle/LruBitmapPoolTest.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import static android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE; import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL; import static android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.os.Build; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = 28) public class LruBitmapPoolTest { private static final int MAX_SIZE = 10; private static final Set ALLOWED_CONFIGS = Collections.singleton(Bitmap.Config.ARGB_8888); private MockStrategy strategy; private LruBitmapPool pool; @Before public void setUp() throws Exception { strategy = new MockStrategy(); pool = new LruBitmapPool(MAX_SIZE, strategy, ALLOWED_CONFIGS); } @Test public void testICanAddAndGetABitmap() { fillPool(pool, 1); pool.put(createMutableBitmap()); assertNotNull(pool.get(100, 100, Bitmap.Config.ARGB_8888)); } @Test public void testImmutableBitmapsAreNotAdded() { Bitmap bitmap = createMutableBitmap(); Bitmap immutable = bitmap.copy(Bitmap.Config.ARGB_8888, /* isMutable= */ false); assertThat(immutable.isMutable()).isFalse(); pool.put(immutable); assertThat(strategy.bitmaps).isEmpty(); } @Test public void testItIsSizeLimited() { fillPool(pool, MAX_SIZE + 2); assertEquals(2, strategy.numRemoves); } @Test public void testBitmapLargerThanPoolIsNotAdded() { strategy = new MockStrategy() { @Override public int getSize(Bitmap bitmap) { return 4; } }; pool = new LruBitmapPool(3, strategy, ALLOWED_CONFIGS); pool.put(createMutableBitmap()); assertEquals(0, strategy.numRemoves); assertEquals(0, strategy.numPuts); } @Test public void testClearMemoryRemovesAllBitmaps() { fillPool(pool, MAX_SIZE); pool.clearMemory(); assertEquals(MAX_SIZE, strategy.numRemoves); } @Test public void testEvictedBitmapsAreRecycled() { fillPool(pool, MAX_SIZE); List bitmaps = new ArrayList<>(MAX_SIZE); bitmaps.addAll(strategy.bitmaps); pool.clearMemory(); for (Bitmap b : bitmaps) { assertTrue(b.isRecycled()); } } @Config(sdk = Build.VERSION_CODES.KITKAT) @Test public void testTrimMemoryUiHiddenOrLessRemovesHalfOfBitmaps_preM() { testTrimMemory(MAX_SIZE, TRIM_MEMORY_UI_HIDDEN, MAX_SIZE / 2); } @Config(sdk = Build.VERSION_CODES.M) @Test public void testTrimMemoryUiHiddenOrLessRemovesHalfOfBitmaps_postM() { testTrimMemory(MAX_SIZE, TRIM_MEMORY_UI_HIDDEN, 0); } @Test public void testTrimMemoryRunningCriticalRemovesHalfOfBitmaps() { testTrimMemory(MAX_SIZE, TRIM_MEMORY_RUNNING_CRITICAL, MAX_SIZE / 2); } @Test public void testTrimMemoryRunningCriticalOrLessRemovesNoBitmapsIfPoolLessThanHalfFull() { testTrimMemory(MAX_SIZE / 2, TRIM_MEMORY_RUNNING_CRITICAL, MAX_SIZE / 2); } @Test public void testTrimMemoryBackgroundOrGreaterRemovesAllBitmaps() { for (int trimLevel : new int[] {TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_COMPLETE}) { testTrimMemory(MAX_SIZE, trimLevel, 0); } } @Test public void testPassesArgb888ToStrategyAsConfigForRequestsWithNullConfigsOnGet() { LruPoolStrategy strategy = mock(LruPoolStrategy.class); LruBitmapPool pool = new LruBitmapPool(100, strategy, ALLOWED_CONFIGS); Bitmap expected = createMutableBitmap(); when(strategy.get(anyInt(), anyInt(), eq(Bitmap.Config.ARGB_8888))).thenReturn(expected); Bitmap result = pool.get(100, 100, null); assertEquals(expected, result); } @Test public void testPassesArgb8888ToStrategyAsConfigForRequestsWithNullConfigsOnGetDirty() { LruPoolStrategy strategy = mock(LruPoolStrategy.class); LruBitmapPool pool = new LruBitmapPool(100, strategy, ALLOWED_CONFIGS); Bitmap expected = createMutableBitmap(); when(strategy.get(anyInt(), anyInt(), eq(Bitmap.Config.ARGB_8888))).thenReturn(expected); Bitmap result = pool.getDirty(100, 100, null); assertEquals(expected, result); } @Test public void get_withNullConfig_andEmptyPool_returnsNewArgb8888Bitmap() { Bitmap result = pool.get(100, 100, /* config= */ null); assertThat(result.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); } @Test public void getDirty_withNullConfig_andEmptyPool_returnsNewArgb8888Bitmap() { Bitmap result = pool.getDirty(100, 100, /* config= */ null); assertThat(result.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); } private void testTrimMemory(int fillSize, int trimLevel, int expectedSize) { MockStrategy strategy = new MockStrategy(); LruBitmapPool pool = new LruBitmapPool(MAX_SIZE, strategy, ALLOWED_CONFIGS); fillPool(pool, fillSize); pool.trimMemory(trimLevel); assertEquals("Failed level=" + trimLevel, expectedSize, strategy.bitmaps.size()); } @Test public void testCanIncreaseSizeDynamically() { int sizeMultiplier = 2; pool.setSizeMultiplier(2); fillPool(pool, MAX_SIZE * sizeMultiplier); assertEquals(0, strategy.numRemoves); } @Test public void testCanDecreaseSizeDynamically() { fillPool(pool, MAX_SIZE); assertEquals(0, strategy.numRemoves); float sizeMultiplier = 0.5f; pool.setSizeMultiplier(sizeMultiplier); assertEquals(Math.round(MAX_SIZE * sizeMultiplier), strategy.numRemoves); } @Test public void testCanResetSizeDynamically() { int sizeMultiplier = 2; pool.setSizeMultiplier(sizeMultiplier); fillPool(pool, MAX_SIZE * sizeMultiplier); pool.setSizeMultiplier(1); assertEquals(MAX_SIZE * sizeMultiplier - MAX_SIZE, strategy.numRemoves); } @Test public void testCanGetCurrentMaxSize() { assertEquals(MAX_SIZE, pool.getMaxSize()); } @Test public void testMaxSizeChangesAfterSizeMultiplier() { pool.setSizeMultiplier(2); assertEquals(2 * MAX_SIZE, pool.getMaxSize()); } @Test public void testBitmapsWithDisallowedConfigsAreIgnored() { pool = new LruBitmapPool(100, strategy, Collections.singleton(Bitmap.Config.ARGB_4444)); Bitmap bitmap = createMutableBitmap(Bitmap.Config.RGB_565); pool.put(bitmap); assertEquals(0, strategy.numPuts); } @Test @Config(sdk = 19) public void testBitmapsWithAllowedNullConfigsAreAllowed() { pool = new LruBitmapPool(100, strategy, Collections.singleton(null)); Bitmap bitmap = createMutableBitmap(); bitmap.setConfig(null); pool.put(bitmap); assertEquals(1, strategy.numPuts); } private void fillPool(LruBitmapPool pool, int fillCount) { for (int i = 0; i < fillCount; i++) { pool.put(createMutableBitmap()); } } private Bitmap createMutableBitmap() { return createMutableBitmap(Bitmap.Config.ARGB_8888); } private Bitmap createMutableBitmap(Bitmap.Config config) { Bitmap bitmap = Bitmap.createBitmap(100, 100, config); assertThat(bitmap.isMutable()).isTrue(); return bitmap; } private static class MockStrategy implements LruPoolStrategy { private final ArrayDeque bitmaps = new ArrayDeque<>(); private int numRemoves; private int numPuts; @Override public void put(Bitmap bitmap) { numPuts++; bitmaps.add(bitmap); } @Override public Bitmap get(int width, int height, Bitmap.Config config) { return bitmaps.isEmpty() ? null : bitmaps.removeLast(); } @Override public Bitmap removeLast() { numRemoves++; return bitmaps.removeLast(); } @Override public String logBitmap(Bitmap bitmap) { return null; } @Override public String logBitmap(int width, int height, Bitmap.Config config) { return null; } @Override public int getSize(Bitmap bitmap) { return 1; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/bitmap_recycle/SizeConfigStrategyTest.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import android.graphics.Bitmap; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.testing.EqualsTester; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class SizeConfigStrategyTest { @Mock private SizeConfigStrategy.KeyPool pool; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void testKeyEquals() { new EqualsTester() .addEqualityGroup( new SizeConfigStrategy.Key(pool, 100, Bitmap.Config.ARGB_8888), new SizeConfigStrategy.Key(pool, 100, Bitmap.Config.ARGB_8888)) .addEqualityGroup(new SizeConfigStrategy.Key(pool, 101, Bitmap.Config.ARGB_8888)) .addEqualityGroup(new SizeConfigStrategy.Key(pool, 100, Bitmap.Config.RGB_565)) .addEqualityGroup(new SizeConfigStrategy.Key(pool, 100, null /*config*/)) .testEquals(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/bitmap_recycle/SizeStrategyKeyTest.java ================================================ package com.bumptech.glide.load.engine.bitmap_recycle; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import com.bumptech.glide.load.engine.bitmap_recycle.SizeStrategy.Key; import com.google.common.testing.EqualsTester; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class SizeStrategyKeyTest { private SizeStrategy.KeyPool keyPool; @Before public void setUp() { keyPool = mock(SizeStrategy.KeyPool.class); } @Test public void testEquality() { Key first = new Key(keyPool); first.init(100); Key second = new Key(keyPool); second.init(100); Key third = new Key(keyPool); third.init(50); new EqualsTester().addEqualityGroup(first, second).addEqualityGroup(third).testEquals(); } @Test public void testReturnsSelfToPoolOnOffer() { Key key = new Key(keyPool); key.offer(); verify(keyPool).offer(eq(key)); } @Test public void testInitSetsSize() { Key key = new Key(keyPool); key.init(100); Key other = new Key(keyPool); other.init(200); assertNotEquals(key, other); key.init(200); assertEquals(key, other); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/cache/DiskLruCacheWrapperTest.java ================================================ package com.bumptech.glide.load.engine.cache; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.mock; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Key; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.tests.Util; import java.io.File; import java.io.IOException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class DiskLruCacheWrapperTest { private DiskCache cache; private byte[] data; private ObjectKey key; private File dir; @Before public void setUp() { dir = ApplicationProvider.getApplicationContext().getCacheDir(); cache = DiskLruCacheWrapper.create(dir, 10 * 1024 * 1024); key = new ObjectKey("test" + Math.random()); data = new byte[] {1, 2, 3, 4, 5, 6}; } @After public void tearDown() { try { cache.clear(); } finally { deleteRecursive(dir); } } private static void deleteRecursive(File file) { if (file.isDirectory()) { File[] files = file.listFiles(); if (files != null) { for (File f : files) { deleteRecursive(f); } } } // GC before delete() to release files on Windows (https://stackoverflow.com/a/4213208/253468) System.gc(); if (!file.delete() && file.exists()) { throw new RuntimeException("Failed to delete: " + file); } } @Test public void testCanInsertAndGet() throws IOException { cache.put( key, new DiskCache.Writer() { @Override public boolean write(@NonNull File file) { try { Util.writeFile(file, data); } catch (IOException e) { fail(e.toString()); } return true; } }); byte[] received = Util.readFile(cache.get(key), data.length); assertArrayEquals(data, received); } @Test public void testDoesNotCommitIfWriterReturnsFalse() { cache.put( key, new DiskCache.Writer() { @Override public boolean write(@NonNull File file) { return false; } }); assertNull(cache.get(key)); } @Test public void testDoesNotCommitIfWriterWritesButReturnsFalse() { cache.put( key, new DiskCache.Writer() { @Override public boolean write(@NonNull File file) { try { Util.writeFile(file, data); } catch (IOException e) { fail(e.toString()); } return false; } }); assertNull(cache.get(key)); } @Test public void testEditIsAbortedIfWriterThrows() throws IOException { try { cache.put( key, new DiskCache.Writer() { @Override public boolean write(@NonNull File file) { throw new RuntimeException("test"); } }); } catch (RuntimeException e) { // Expected. } cache.put( key, new DiskCache.Writer() { @Override public boolean write(@NonNull File file) { try { Util.writeFile(file, data); } catch (IOException e) { fail(e.toString()); } return true; } }); byte[] received = Util.readFile(cache.get(key), data.length); assertArrayEquals(data, received); } // Tests #2465. @Test public void clearDiskCache_afterOpeningDiskCache_andDeleteDirectoryOutsideGlide_doesNotThrow() { assumeTrue("A file handle is likely open, so cannot delete dir", !Util.isWindows()); DiskCache cache = DiskLruCacheWrapper.create(dir, 1024 * 1024); cache.get(mock(Key.class)); deleteRecursive(dir); cache.clear(); } // Tests #2465. @Test public void get_afterDeleteDirectoryOutsideGlideAndClose_doesNotThrow() { assumeTrue("A file handle is likely open, so cannot delete dir", !Util.isWindows()); DiskCache cache = DiskLruCacheWrapper.create(dir, 1024 * 1024); cache.get(mock(Key.class)); deleteRecursive(dir); cache.clear(); cache.get(mock(Key.class)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/cache/LruCacheTest.java ================================================ package com.bumptech.glide.load.engine.cache; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.util.LruCache; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class LruCacheTest { private static final int SIZE = 10; private LruCache cache; private CacheListener listener; private String currentKey; @Before public void setUp() { currentKey = ""; listener = mock(CacheListener.class); cache = new TestLruCache(SIZE, listener); when(listener.getSize(any())).thenReturn(1); } @Test public void testCanAddAndRetrieveItem() { String key = getKey(); Object object = new Object(); cache.put(key, object); assertEquals(object, cache.get(key)); } @Test public void testCanPutNullItemWithoutChangingSize() { String key = getKey(); cache.put(key, null); for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } verify(listener, never()).onItemRemoved(any()); } @Test public void testReplacingNonNullItemWithNullItemDecreasesSize() { String key = getKey(); Object initialValue = new Object(); cache.put(key, initialValue); cache.put(key, null); for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } verify(listener).onItemRemoved(initialValue); } @Test public void testReplacingNullItemWIthNullItemIncreasesSize() { String key = getKey(); cache.put(key, null); cache.put(key, new Object()); for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } verify(listener).onItemRemoved(any()); } @Test public void testReplacingNonNullItemWithNonNullItemUpdatesSize() { String key = getKey(); Object initialValue = new Object(); cache.put(key, initialValue); cache.put(key, new Object()); for (int i = 0; i < SIZE - 1; i++) { cache.put(getKey(), new Object()); } verify(listener).onItemRemoved(initialValue); verify(listener, never()).onItemRemoved(not(eq(initialValue))); } @Test public void testCacheContainsAddedBitmap() { final String key = getKey(); cache.put(key, new Object()); assertTrue(cache.contains(key)); } @Test public void testEmptyCacheDoesNotContainKey() { assertFalse(cache.contains(getKey())); } @Test public void testItIsSizeLimited() { for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } verify(listener, never()).onItemRemoved(any()); cache.put(getKey(), new Object()); verify(listener).onItemRemoved(any()); } @Test public void testLeastRecentlyAddKeyEvictedFirstIfGetsAreEqual() { Object first = new Object(); cache.put(getKey(), first); for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } verify(listener).onItemRemoved(eq(first)); verify(listener, times(1)).onItemRemoved(any(Object.class)); } @Test public void testLeastRecentlyUsedKeyEvictedFirst() { String mostRecentlyUsedKey = getKey(); Object mostRecentlyUsedObject = new Object(); String leastRecentlyUsedKey = getKey(); Object leastRecentlyUsedObject = new Object(); cache.put(mostRecentlyUsedKey, mostRecentlyUsedObject); cache.put(leastRecentlyUsedKey, leastRecentlyUsedObject); cache.get(mostRecentlyUsedKey); for (int i = 0; i < SIZE - 1; i++) { cache.put(getKey(), new Object()); } verify(listener).onItemRemoved(eq(leastRecentlyUsedObject)); verify(listener, times(1)).onItemRemoved(any(Object.class)); } @Test public void testItemLargerThanCacheIsImmediatelyEvicted() { Object tooLarge = new Object(); when(listener.getSize(eq(tooLarge))).thenReturn(SIZE + 1); cache.put(getKey(), tooLarge); verify(listener).onItemRemoved(eq(tooLarge)); } @Test public void testItemLargerThanCacheDoesNotCauseAdditionalEvictions() { cache.put(getKey(), new Object()); Object tooLarge = new Object(); when(listener.getSize(eq(tooLarge))).thenReturn(SIZE + 1); cache.put(getKey(), tooLarge); verify(listener, times(1)).onItemRemoved(any()); } @Test public void testClearMemoryRemovesAllItems() { String first = getKey(); String second = getKey(); cache.put(first, new Object()); cache.put(second, new Object()); cache.clearMemory(); assertFalse(cache.contains(first)); assertFalse(cache.contains(second)); } @Test public void testCanPutSameItemMultipleTimes() { String key = getKey(); Object value = new Object(); for (int i = 0; i < SIZE * 2; i++) { cache.put(key, value); } verify(listener, never()).onItemRemoved(any()); } @Test public void put_withSameKeyAndValueTwice_doesNotEvictItems() { String key = getKey(); Object value = new Object(); cache.put(key, value); cache.put(key, value); verify(listener, never()).onItemRemoved(any()); } @Test public void put_withExistingNullValue_doesNotNotifyListener() { String key = getKey(); cache.put(key, /* item= */ null); cache.put(key, new Object()); verify(listener, never()).onItemRemoved(any()); } @Test public void put_withNullValue_withSizeGreaterThanMaximum_notifiesListener() { String key = getKey(); when(listener.getSize(null)).thenReturn((int) (cache.getMaxSize() * 2)); cache.put(key, null); verify(listener).onItemRemoved(any()); } @Test public void testCanIncreaseSizeDynamically() { int sizeMultiplier = 2; cache.setSizeMultiplier(sizeMultiplier); for (int i = 0; i < SIZE * sizeMultiplier; i++) { cache.put(getKey(), new Object()); } verify(listener, never()).onItemRemoved(any()); } @Test public void testCanDecreaseSizeDynamically() { for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } verify(listener, never()).onItemRemoved(any()); float smallerMultiplier = 0.4f; cache.setSizeMultiplier(smallerMultiplier); verify(listener, times((int) (SIZE * (1 - smallerMultiplier)))).onItemRemoved(any()); } @Test public void testCanResetSizeDynamically() { int sizeMultiplier = 2; cache.setSizeMultiplier(sizeMultiplier); for (int i = 0; i < SIZE * sizeMultiplier; i++) { cache.put(getKey(), new Object()); } cache.setSizeMultiplier(1); verify(listener, times((sizeMultiplier * SIZE) - SIZE)).onItemRemoved(any()); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfMultiplierLessThanZero() { cache.setSizeMultiplier(-1); } @Test public void testCanHandleZeroAsMultiplier() { for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } cache.setSizeMultiplier(0); verify(listener, times(SIZE)).onItemRemoved(any()); } @Test public void testCanRemoveKeys() { String key = getKey(); Object value = new Object(); cache.put(key, value); cache.remove(key); assertNull(cache.get(key)); assertFalse(cache.contains(key)); } @Test public void testDecreasesSizeWhenRemovesKey() { String key = getKey(); Object value = new Object(); cache.put(key, value); for (int i = 0; i < SIZE - 1; i++) { cache.put(getKey(), value); } cache.remove(key); cache.put(key, value); verify(listener, never()).onItemRemoved(any()); } @Test public void testDoesNotCallListenerWhenRemovesKey() { String key = getKey(); cache.put(key, new Object()); cache.remove(key); verify(listener, never()).onItemRemoved(any()); } @Test public void testGetMaxSizeReturnsCurrentMaxSizeOfCache() { assertEquals(SIZE, cache.getMaxSize()); } @Test public void setSizeMultiplier_withItemWhoseSizeDecreasesAfterAdd_doesNotCrash() { Object itemWhoseSizeWillChange = new Object(); when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn(SIZE / 2); cache.put(getKey(), itemWhoseSizeWillChange); cache.setSizeMultiplier(0); } @Test public void getCurrentSize_afterRemovingItemWhoseSizeChanged_returnsZero() { Object itemWhoseSizeWillChange = new Object(); when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn(SIZE / 2); String key = getKey(); cache.put(key, itemWhoseSizeWillChange); cache.remove(key); assertThat(cache.getCurrentSize()).isEqualTo(0); } @Test public void clearMemory_afterRemovingItemWhoseSizeChanged_doesNotCrash() { Object itemWhoseSizeWillChange = new Object(); when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn((SIZE / 2) - 1); String key = getKey(); cache.put(key, itemWhoseSizeWillChange); cache.remove(key); cache.clearMemory(); } @Test public void getCurrentSize_afterUpdatingItemWhoseSizeChanged_returnsTheNewSize() { Object itemWhoseSizeWillChange = new Object(); when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn((SIZE / 2) - 1); String key = getKey(); cache.put(key, itemWhoseSizeWillChange); cache.put(key, new Object()); assertThat(cache.getCurrentSize()).isEqualTo(1); } @Test public void clearMemory_afterUpdatingItemWhoseSizeChanged_doesNotCrash() { Object itemWhoseSizeWillChange = new Object(); when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn((SIZE / 2) - 1); String key = getKey(); cache.put(key, itemWhoseSizeWillChange); cache.put(key, new Object()); cache.clearMemory(); } @Test public void testGetMaxSizeChangesIfMaxSizeChanges() { int multiplier = 2; cache.setSizeMultiplier(multiplier); assertEquals(SIZE * multiplier, cache.getMaxSize()); } @Test public void getCurrentSizeReturnsZeroForEmptyCache() { assertEquals(0, cache.getCurrentSize()); } @Test public void testGetCurrentSizeIncreasesAsSizeIncreases() { cache.put(getKey(), new Object()); assertEquals(1, cache.getCurrentSize()); cache.put(getKey(), new Object()); assertEquals(2, cache.getCurrentSize()); } @Test public void testGetCurrentSizeDoesNotChangeWhenSizeMultiplierChangesIfNoItemsAreEvicted() { cache.put(getKey(), new Object()); assertEquals(1, cache.getCurrentSize()); cache.setSizeMultiplier(2); assertEquals(1, cache.getCurrentSize()); } @Test public void testGetCurrentSizeChangesIfItemsAreEvictedWhenSizeMultiplierChanges() { for (int i = 0; i < SIZE; i++) { cache.put(getKey(), new Object()); } assertEquals(SIZE, cache.getCurrentSize()); cache.setSizeMultiplier(0.5f); assertEquals(SIZE / 2, cache.getCurrentSize()); } private String getKey() { currentKey += "1"; return currentKey; } private interface CacheListener { void onItemRemoved(Object item); int getSize(Object item); } private static class TestLruCache extends LruCache { private final CacheListener listener; TestLruCache(int size, CacheListener listener) { super(size); this.listener = listener; } @Override protected void onItemEvicted(@NonNull String key, @Nullable Object item) { listener.onItemRemoved(item); } @Override protected int getSize(@Nullable Object item) { return listener.getSize(item); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/cache/LruResourceCacheTest.java ================================================ package com.bumptech.glide.load.engine.cache; import static com.bumptech.glide.load.engine.cache.MemoryCache.ResourceRemovedListener; import static com.bumptech.glide.tests.Util.anyResource; import static com.bumptech.glide.tests.Util.mockResource; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ComponentCallbacks2; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.LruCache; import java.security.MessageDigest; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class LruResourceCacheTest { @Test public void put_withExistingItem_updatesSizeCorrectly() { PutWithExistingEntryHarness harness = new PutWithExistingEntryHarness(); harness.cache.put(harness.key, harness.first); harness.cache.put(harness.key, harness.second); assertThat(harness.cache.getCurrentSize()).isEqualTo(harness.second.getSize()); } @Test public void put_withExistingItem_evictsExistingItem() { PutWithExistingEntryHarness harness = new PutWithExistingEntryHarness(); harness.cache.put(harness.key, harness.first); harness.cache.put(harness.key, harness.second); verify(harness.listener).onResourceRemoved(harness.first); } @Test public void get_afterPutWithExistingItem_returnsNewItem() { PutWithExistingEntryHarness harness = new PutWithExistingEntryHarness(); harness.cache.put(harness.key, harness.first); harness.cache.put(harness.key, harness.second); assertThat(harness.cache.get(harness.key)).isEqualTo(harness.second); } @Test public void onItemEvicted_withNullValue_doesNotNotifyListener() { PutWithExistingEntryHarness harness = new PutWithExistingEntryHarness(); harness.cache.onItemEvicted(new MockKey(), null); verify(harness.listener, never()).onResourceRemoved(anyResource()); } @Test public void clearMemory_afterPutWithExistingItem_evictsOnlyNewItem() { PutWithExistingEntryHarness harness = new PutWithExistingEntryHarness(); harness.cache.put(harness.key, harness.first); harness.cache.put(harness.key, harness.second); verify(harness.listener).onResourceRemoved(harness.first); verify(harness.listener, never()).onResourceRemoved(harness.second); harness.cache.clearMemory(); verify(harness.listener, times(1)).onResourceRemoved(harness.first); verify(harness.listener).onResourceRemoved(harness.second); } @Test public void testTrimMemoryBackground() { TrimClearMemoryCacheHarness harness = new TrimClearMemoryCacheHarness(); harness.resourceCache.trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); verify(harness.listener).onResourceRemoved(eq(harness.first)); verify(harness.listener).onResourceRemoved(eq(harness.second)); } @Test public void testTrimMemoryModerate() { TrimClearMemoryCacheHarness harness = new TrimClearMemoryCacheHarness(); harness.resourceCache.trimMemory(ComponentCallbacks2.TRIM_MEMORY_MODERATE); verify(harness.listener).onResourceRemoved(harness.first); verify(harness.listener).onResourceRemoved(harness.second); } @Test public void testTrimMemoryUiHidden() { TrimClearMemoryCacheHarness harness = new TrimClearMemoryCacheHarness(); harness.resourceCache.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); verify(harness.listener).onResourceRemoved(harness.first); verify(harness.listener, never()).onResourceRemoved(harness.second); } @Test public void testTrimMemoryRunningCritical() { TrimClearMemoryCacheHarness harness = new TrimClearMemoryCacheHarness(); harness.resourceCache.trimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL); verify(harness.listener).onResourceRemoved(harness.first); verify(harness.listener, never()).onResourceRemoved(harness.second); } @Test public void testResourceRemovedListenerIsNotifiedWhenResourceIsRemoved() { LruResourceCache resourceCache = new LruResourceCache(100); Resource resource = mockResource(); when(resource.getSize()).thenReturn(200); ResourceRemovedListener listener = mock(ResourceRemovedListener.class); resourceCache.setResourceRemovedListener(listener); resourceCache.put(new MockKey(), resource); verify(listener).onResourceRemoved(eq(resource)); } @Test public void testSizeIsBasedOnResource() { LruResourceCache resourceCache = new LruResourceCache(100); Resource first = getResource(50); MockKey firstKey = new MockKey(); resourceCache.put(firstKey, first); Resource second = getResource(50); MockKey secondKey = new MockKey(); resourceCache.put(secondKey, second); assertTrue(resourceCache.contains(firstKey)); assertTrue(resourceCache.contains(secondKey)); Resource third = getResource(50); MockKey thirdKey = new MockKey(); resourceCache.put(thirdKey, third); assertFalse(resourceCache.contains(firstKey)); assertTrue(resourceCache.contains(secondKey)); assertTrue(resourceCache.contains(thirdKey)); } @Test public void testPreventEviction() { final MemoryCache cache = new LruResourceCache(100); final Resource first = getResource(30); final Key firstKey = new MockKey(); cache.put(firstKey, first); Resource second = getResource(30); Key secondKey = new MockKey(); cache.put(secondKey, second); Resource third = getResource(30); Key thirdKey = new MockKey(); cache.put(thirdKey, third); cache.setResourceRemovedListener( new ResourceRemovedListener() { @Override public void onResourceRemoved(@NonNull Resource removed) { if (removed == first) { cache.put(firstKey, first); } } }); // trims from 100 to 50, having 30+30+30 items, it should trim to 1 item cache.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); // and that 1 item must be first, because it's forced to return to cache in the listener @SuppressWarnings("unchecked") LruCache> lruCache = (LruCache>) cache; assertTrue(lruCache.contains(firstKey)); assertFalse(lruCache.contains(secondKey)); assertFalse(lruCache.contains(thirdKey)); } private Resource getResource(int size) { Resource resource = mockResource(); when(resource.getSize()).thenReturn(size); return resource; } private static class MockKey implements Key { @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(toString().getBytes(CHARSET)); } } private static class PutWithExistingEntryHarness { final LruResourceCache cache = new LruResourceCache(100); final Resource first = mockResource(); final Resource second = mockResource(); final ResourceRemovedListener listener = mock(ResourceRemovedListener.class); final Key key = new MockKey(); PutWithExistingEntryHarness() { when(first.getSize()).thenReturn(50); when(second.getSize()).thenReturn(50); cache.setResourceRemovedListener(listener); } } private static class TrimClearMemoryCacheHarness { final LruResourceCache resourceCache = new LruResourceCache(100); final Resource first = mockResource(); final Resource second = mockResource(); final ResourceRemovedListener listener = mock(ResourceRemovedListener.class); TrimClearMemoryCacheHarness() { when(first.getSize()).thenReturn(50); when(second.getSize()).thenReturn(50); resourceCache.put(new MockKey(), first); resourceCache.put(new MockKey(), second); resourceCache.setResourceRemovedListener(listener); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/cache/MemorySizeCalculatorTest.java ================================================ package com.bumptech.glide.load.engine.cache; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.Context; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.engine.cache.MemorySizeCalculatorTest.LowRamActivityManager; import com.bumptech.glide.tests.Util; import com.google.common.collect.Range; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowActivityManager; @RunWith(RobolectricTestRunner.class) @Config(sdk = 19, shadows = LowRamActivityManager.class) public class MemorySizeCalculatorTest { private MemorySizeHarness harness; private int initialSdkVersion; @Before public void setUp() { initialSdkVersion = Build.VERSION.SDK_INT; harness = new MemorySizeHarness(); } @After public void tearDown() { Util.setSdkVersionInt(initialSdkVersion); } @Test public void testDefaultMemoryCacheSizeIsTwiceScreenSize() { Shadows.shadowOf(harness.activityManager).setMemoryClass(getLargeEnoughMemoryClass()); float memoryCacheSize = harness.getCalculator().getMemoryCacheSize(); assertThat(memoryCacheSize).isEqualTo(harness.getScreenSize() * harness.memoryCacheScreens); } @Test public void testCanSetCustomMemoryCacheSize() { harness.memoryCacheScreens = 9.5f; Shadows.shadowOf(harness.activityManager).setMemoryClass(getLargeEnoughMemoryClass()); float memoryCacheSize = harness.getCalculator().getMemoryCacheSize(); assertThat(memoryCacheSize).isEqualTo(harness.getScreenSize() * harness.memoryCacheScreens); } @Test public void testDefaultMemoryCacheSizeIsLimitedByMemoryClass() { final int memoryClassBytes = Math.round(harness.getScreenSize() * harness.memoryCacheScreens * harness.sizeMultiplier); Shadows.shadowOf(harness.activityManager).setMemoryClass(memoryClassBytes / (1024 * 1024)); float memoryCacheSize = harness.getCalculator().getMemoryCacheSize(); assertThat(memoryCacheSize).isIn(Range.atMost(memoryClassBytes * harness.sizeMultiplier)); } @Test public void testDefaultBitmapPoolSize() { Shadows.shadowOf(harness.activityManager).setMemoryClass(getLargeEnoughMemoryClass()); float bitmapPoolSize = harness.getCalculator().getBitmapPoolSize(); assertThat(bitmapPoolSize).isEqualTo(harness.getScreenSize() * harness.bitmapPoolScreens); } @Test public void testCanSetCustomBitmapPoolSize() { harness.bitmapPoolScreens = 2f; Shadows.shadowOf(harness.activityManager).setMemoryClass(getLargeEnoughMemoryClass()); float bitmapPoolSize = harness.getCalculator().getBitmapPoolSize(); assertThat(bitmapPoolSize).isEqualTo(harness.getScreenSize() * harness.bitmapPoolScreens); } @Test public void testDefaultBitmapPoolSizeIsLimitedByMemoryClass() { final int memoryClassBytes = Math.round(harness.getScreenSize() * harness.bitmapPoolScreens * harness.sizeMultiplier); Shadows.shadowOf(harness.activityManager).setMemoryClass(memoryClassBytes / (1024 * 1024)); int bitmapPoolSize = harness.getCalculator().getBitmapPoolSize(); assertThat((float) bitmapPoolSize) .isIn(Range.atMost(memoryClassBytes * harness.sizeMultiplier)); } @Test public void testCumulativePoolAndMemoryCacheSizeAreLimitedByMemoryClass() { final int memoryClassBytes = Math.round( harness.getScreenSize() * (harness.bitmapPoolScreens + harness.memoryCacheScreens) * harness.sizeMultiplier); Shadows.shadowOf(harness.activityManager).setMemoryClass(memoryClassBytes / (1024 * 1024)); int memoryCacheSize = harness.getCalculator().getMemoryCacheSize(); int bitmapPoolSize = harness.getCalculator().getBitmapPoolSize(); assertThat((float) memoryCacheSize + bitmapPoolSize) .isIn(Range.atMost(memoryClassBytes * harness.sizeMultiplier)); } @Test public void testCumulativePoolAndMemoryCacheSizesAreSmallerOnLowMemoryDevices() { Shadows.shadowOf(harness.activityManager).setMemoryClass(getLargeEnoughMemoryClass() / 2); final int normalMemoryCacheSize = harness.getCalculator().getMemoryCacheSize(); final int normalBitmapPoolSize = harness.getCalculator().getBitmapPoolSize(); Util.setSdkVersionInt(10); // Keep the bitmap pool size constant, even though normally it would change. harness.byteArrayPoolSizeBytes *= 2; final int smallMemoryCacheSize = harness.getCalculator().getMemoryCacheSize(); final int smallBitmapPoolSize = harness.getCalculator().getBitmapPoolSize(); assertThat(smallMemoryCacheSize).isLessThan(normalMemoryCacheSize); assertThat(smallBitmapPoolSize).isLessThan(normalBitmapPoolSize); } @Test public void testByteArrayPoolSize_withLowRamDevice_isHalfTheSpecifiedBytes() { LowRamActivityManager activityManager = Shadow.extract(harness.activityManager); activityManager.setMemoryClass(getLargeEnoughMemoryClass()); activityManager.setIsLowRam(); int byteArrayPoolSize = harness.getCalculator().getArrayPoolSizeInBytes(); assertThat(byteArrayPoolSize).isEqualTo(harness.byteArrayPoolSizeBytes / 2); } private int getLargeEnoughMemoryClass() { float totalScreenBytes = harness.getScreenSize() * (harness.bitmapPoolScreens + harness.memoryCacheScreens); float totalBytes = totalScreenBytes + harness.byteArrayPoolSizeBytes; // Memory class is in mb, not bytes! float totalMb = totalBytes / (1024 * 1024); float memoryClassMb = totalMb / harness.sizeMultiplier; return (int) Math.ceil(memoryClassMb); } private static class MemorySizeHarness { final int pixelSize = 500; final int bytesPerPixel = MemorySizeCalculator.BYTES_PER_ARGB_8888_PIXEL; float memoryCacheScreens = MemorySizeCalculator.Builder.MEMORY_CACHE_TARGET_SCREENS; float bitmapPoolScreens = MemorySizeCalculator.Builder.BITMAP_POOL_TARGET_SCREENS; final float sizeMultiplier = MemorySizeCalculator.Builder.MAX_SIZE_MULTIPLIER; int byteArrayPoolSizeBytes = MemorySizeCalculator.Builder.ARRAY_POOL_SIZE_BYTES; final ActivityManager activityManager = (ActivityManager) ApplicationProvider.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE); final MemorySizeCalculator.ScreenDimensions screenDimensions = mock(MemorySizeCalculator.ScreenDimensions.class); MemorySizeCalculator getCalculator() { when(screenDimensions.getWidthPixels()).thenReturn(pixelSize); when(screenDimensions.getHeightPixels()).thenReturn(pixelSize); return new MemorySizeCalculator.Builder(ApplicationProvider.getApplicationContext()) .setMemoryCacheScreens(memoryCacheScreens) .setBitmapPoolScreens(bitmapPoolScreens) .setMaxSizeMultiplier(sizeMultiplier) .setActivityManager(activityManager) .setScreenDimensions(screenDimensions) .setArrayPoolSize(byteArrayPoolSizeBytes) .build(); } int getScreenSize() { return pixelSize * pixelSize * bytesPerPixel; } } @Implements(ActivityManager.class) public static final class LowRamActivityManager extends ShadowActivityManager { private boolean isLowRam; void setIsLowRam() { this.isLowRam = true; } @Implementation @Override public boolean isLowRamDevice() { return isLowRam; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/cache/SafeKeyGeneratorTest.java ================================================ package com.bumptech.glide.load.engine.cache; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertTrue; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import java.security.MessageDigest; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class SafeKeyGeneratorTest { private SafeKeyGenerator keyGenerator; private int nextId; @Before public void setUp() { nextId = 0; keyGenerator = new SafeKeyGenerator(); } @Test public void testKeysAreValidForDiskCache() { final Pattern diskCacheRegex = Pattern.compile("[a-z0-9_-]{64}"); for (int i = 0; i < 1000; i++) { String key = getRandomKeyFromGenerator(); Matcher matcher = diskCacheRegex.matcher(key); assertTrue(key, matcher.matches()); } } private String getRandomKeyFromGenerator() { return keyGenerator.getSafeKey(new MockKey(getNextId())); } private String getNextId() { return String.valueOf(nextId++); } private static final class MockKey implements Key { private final String id; MockKey(String id) { this.id = id; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update(id.getBytes(CHARSET)); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/executor/GlideExecutorTest.java ================================================ package com.bumptech.glide.load.engine.executor; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GlideExecutorTest { @Test public void testOnExecuteDecorator_isCalledAndCanDecorateRunnable() throws InterruptedException { final CountDownLatch decoratorCalled = new CountDownLatch(1); final CountDownLatch decoratedRunnableExecuted = new CountDownLatch(1); GlideExecutor executor = GlideExecutor.newDiskCacheBuilder() .experimentalSetOnExecuteDecorator( new Function() { @Override public Runnable apply(Runnable runnable) { decoratorCalled.countDown(); return new Runnable() { @Override public void run() { decoratedRunnableExecuted.countDown(); runnable.run(); } }; } }) .build(); final CountDownLatch originalRunnableExecuted = new CountDownLatch(1); executor.execute( new Runnable() { @Override public void run() { originalRunnableExecuted.countDown(); } }); assertThat(decoratorCalled.await(100, TimeUnit.MILLISECONDS)).isTrue(); assertThat(decoratedRunnableExecuted.await(100, TimeUnit.MILLISECONDS)).isTrue(); assertThat(originalRunnableExecuted.await(100, TimeUnit.MILLISECONDS)).isTrue(); executor.shutdown(); executor.awaitTermination(500, TimeUnit.MILLISECONDS); } @Test public void testOnExecuteDecorator_notDecorated_decoratorNotCalled() throws InterruptedException { final CountDownLatch decoratorCalled = new CountDownLatch(1); final CountDownLatch decoratedRunnableExecuted = new CountDownLatch(1); GlideExecutor executor = GlideExecutor.newDiskCacheBuilder().build(); final CountDownLatch originalRunnableExecuted = new CountDownLatch(1); executor.execute( new Runnable() { @Override public void run() { originalRunnableExecuted.countDown(); } }); assertThat(decoratorCalled.await(100, TimeUnit.MILLISECONDS)).isFalse(); assertThat(decoratedRunnableExecuted.await(100, TimeUnit.MILLISECONDS)).isFalse(); assertThat(originalRunnableExecuted.await(100, TimeUnit.MILLISECONDS)).isTrue(); executor.shutdown(); executor.awaitTermination(500, TimeUnit.MILLISECONDS); } @Test public void testLoadsAreExecutedInOrder() throws InterruptedException { final List resultPriorities = Collections.synchronizedList(new ArrayList()); CountDownLatch latch = new CountDownLatch(1); GlideExecutor executor = GlideExecutor.newDiskCacheExecutor(); for (int i = 5; i > 0; i--) { executor.execute( new MockRunnable( i, new MockRunnable.OnRun() { @Override public void onRun(int priority) { try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } resultPriorities.add(priority); } })); } latch.countDown(); executor.shutdown(); executor.awaitTermination(500, TimeUnit.MILLISECONDS); // Since no jobs are queued, the first item added will be run immediately, regardless of // priority. assertThat(resultPriorities).containsExactly(5, 1, 2, 3, 4).inOrder(); } private static final class MockRunnable implements Runnable, Comparable { private final int priority; private final OnRun onRun; @Override public int compareTo(@NonNull MockRunnable another) { return priority - another.priority; } interface OnRun { void onRun(int priority); } MockRunnable(int priority, OnRun onRun) { this.priority = priority; this.onRun = onRun; } @Override public void run() { onRun.onRun(priority); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillRunnerTest.java ================================================ package com.bumptech.glide.load.engine.prefill; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.anyResource; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.os.Handler; import android.util.Log; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.load.engine.cache.MemoryCacheAdapter; import com.bumptech.glide.load.resource.bitmap.BitmapResource; import com.bumptech.glide.tests.Util.CreateBitmap; import com.bumptech.glide.util.Util; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapPreFillRunnerTest { @Mock private BitmapPreFillRunner.Clock clock; @Mock private BitmapPool pool; @Mock private MemoryCache cache; @Mock private Handler mainHandler; private final List addedBitmaps = new ArrayList<>(); @Before public void setUp() { MockitoAnnotations.initMocks(this); doAnswer(new AddBitmapPoolAnswer(addedBitmaps)).when(pool).put(any(Bitmap.class)); when(pool.getDirty(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenAnswer(new CreateBitmap()); when(cache.put(any(Key.class), anyResource())) .thenAnswer(new AddBitmapCacheAnswer(addedBitmaps)); } private BitmapPreFillRunner getHandler(Map allocationOrder) { return new BitmapPreFillRunner( pool, cache, new PreFillQueue(allocationOrder), clock, mainHandler); } @Test public void testAllocatesABitmapPerSizeInAllocationOrder() { PreFillType size = new PreFillType.Builder(100).setConfig(Bitmap.Config.ARGB_8888).build(); final int toAdd = 3; Map allocationOrder = new HashMap<>(); allocationOrder.put(size, toAdd); BitmapPreFillRunner handler = getHandler(allocationOrder); handler.run(); Bitmap expected = Bitmap.createBitmap(size.getWidth(), size.getHeight(), size.getConfig()); // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed // assertThat(addedBitmaps).containsExactly(expected, expected, expected); } @Test public void testAllocatesBitmapsInOrderGivenByAllocationOrder() { PreFillType smallWidth = new PreFillType.Builder(50, 100).setConfig(Bitmap.Config.ARGB_8888).build(); PreFillType smallHeight = new PreFillType.Builder(100, 50).setConfig(Bitmap.Config.RGB_565).build(); PreFillType[] expectedOrder = new PreFillType[] { smallWidth, smallHeight, smallWidth, smallHeight, }; HashMap allocationOrder = new HashMap<>(); allocationOrder.put(smallWidth, 2); allocationOrder.put(smallHeight, 2); BitmapPreFillRunner handler = getHandler(allocationOrder); handler.run(); Bitmap[] expectedBitmaps = new Bitmap[expectedOrder.length]; for (int i = 0; i < expectedBitmaps.length; i++) { PreFillType current = expectedOrder[i]; expectedBitmaps[i] = Bitmap.createBitmap(current.getWidth(), current.getHeight(), current.getConfig()); } Bitmap current = addedBitmaps.get(0); for (int i = 1; i < addedBitmaps.size(); i++) { assertNotEquals(current, addedBitmaps.get(i)); current = addedBitmaps.get(i); } assertThat(addedBitmaps).hasSize(4); } @Test public void testStopsAllocatingBitmapsUntilNextIdleCallIfAllocationsTakeLongerThanLimit() { PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(size, 3); when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); BitmapPreFillRunner handler = getHandler(allocationOrder); handler.run(); assertThat(addedBitmaps).hasSize(1); handler.run(); assertThat(addedBitmaps).hasSize(3); } @Test public void testPreFillHandlerDoesNotPostIfHasNoBitmapsToAllocate() { BitmapPreFillRunner handler = getHandler(new HashMap()); handler.run(); verify(mainHandler, never()).postDelayed(any(Runnable.class), anyLong()); } @Test public void testPreFillHandlerPostsIfHasBitmapsToAllocateAfterRunning() { PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(size, 2); BitmapPreFillRunner handler = getHandler(allocationOrder); when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); handler.run(); verify(mainHandler).postDelayed(eq(handler), anyLong()); } @Test public void testPreFillHandlerPostsWithBackoffIfHasBitmapsToAllocateAfterRunning() { PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(size, 100); BitmapPreFillRunner handler = getHandler(allocationOrder); when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); handler.run(); verify(mainHandler).postDelayed(eq(handler), eq(BitmapPreFillRunner.INITIAL_BACKOFF_MS)); when(clock.now()) .thenReturn(BitmapPreFillRunner.MAX_DURATION_MS) .thenReturn( BitmapPreFillRunner.MAX_DURATION_MS + BitmapPreFillRunner.INITIAL_BACKOFF_MS * BitmapPreFillRunner.BACKOFF_RATIO); handler.run(); verify(mainHandler) .postDelayed( eq(handler), eq(BitmapPreFillRunner.INITIAL_BACKOFF_MS * BitmapPreFillRunner.BACKOFF_RATIO)); when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); handler.run(); when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); handler.run(); when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); handler.run(); when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); handler.run(); verify(mainHandler, atLeastOnce()) .postDelayed(eq(handler), eq(BitmapPreFillRunner.MAX_BACKOFF_MS)); } @Test public void testPreFillHandlerDoesNotPostIfHasBitmapsButIsCancelled() { PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(size, 2); BitmapPreFillRunner handler = getHandler(allocationOrder); when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS); handler.cancel(); handler.run(); verify(mainHandler, never()).postDelayed(any(Runnable.class), anyLong()); } @Test public void testAddsBitmapsToMemoryCacheIfMemoryCacheHasEnoughSpaceRemaining() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(cache.getMaxSize()).thenReturn(Long.valueOf(Util.getBitmapByteSize(bitmap))); PreFillType size = new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight()) .setConfig(bitmap.getConfig()) .build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(size, 1); getHandler(allocationOrder).run(); verify(cache).put(any(Key.class), anyResource()); verify(pool, never()).put(any(Bitmap.class)); // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed // assertThat(addedBitmaps).containsExactly(bitmap); } @Test public void testAddsBitmapsToBitmapPoolIfMemoryCacheIsFull() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(cache.getMaxSize()).thenReturn(0L); PreFillType size = new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight()) .setConfig(bitmap.getConfig()) .build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(size, 1); getHandler(allocationOrder).run(); verify(cache, never()).put(any(Key.class), anyResource()); // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed // verify(pool).put(eq(bitmap)); // assertThat(addedBitmaps).containsExactly(bitmap); } @Test public void testAddsBitmapsToPoolIfMemoryCacheIsNotFullButCannotFitBitmap() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(cache.getMaxSize()).thenReturn((long) Util.getBitmapByteSize(bitmap) / 2); PreFillType size = new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight()) .setConfig(bitmap.getConfig()) .build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(size, 1); getHandler(allocationOrder).run(); verify(cache, never()).put(any(Key.class), anyResource()); // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed // verify(pool).put(eq(bitmap)); // assertThat(addedBitmaps).containsExactly(bitmap); } @Test public void testDoesAGetFromPoolBeforeAddingForEachSize() { Bitmap first = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); PreFillType firstSize = new PreFillType.Builder(first.getWidth(), first.getHeight()) .setConfig(first.getConfig()) .build(); Bitmap second = Bitmap.createBitmap(200, 200, Bitmap.Config.RGB_565); PreFillType secondSize = new PreFillType.Builder(second.getWidth(), second.getHeight()) .setConfig(second.getConfig()) .build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(firstSize, 1); allocationOrder.put(secondSize, 1); getHandler(allocationOrder).run(); InOrder firstOrder = inOrder(pool); firstOrder .verify(pool) .getDirty(eq(first.getWidth()), eq(first.getHeight()), eq(first.getConfig())); // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed // firstOrder.verify(pool).put(eq(first)); InOrder secondOrder = inOrder(pool); secondOrder .verify(pool) .getDirty(eq(second.getWidth()), eq(second.getHeight()), eq(second.getConfig())); // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed // secondOrder.verify(pool).put(eq(second)); } @Test public void testDoesNotGetMoreThanOncePerSize() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); PreFillType size = new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight()) .setConfig(bitmap.getConfig()) .build(); final int numBitmaps = 5; Map allocationOrder = new HashMap<>(); allocationOrder.put(size, numBitmaps); getHandler(allocationOrder).run(); InOrder order = inOrder(pool); order .verify(pool) .getDirty(eq(bitmap.getWidth()), eq(bitmap.getHeight()), eq(bitmap.getConfig())); // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed // order.verify(pool, times(numBitmaps)).put(eq(bitmap)); } @Test public void allocate_whenBitmapPoolIsAtCapacity_doesNotLogWithRecycledBitmap() { ShadowLog.setLoggable(BitmapPreFillRunner.TAG, Log.VERBOSE); int dimensions = 10; Bitmap.Config config = Bitmap.Config.ARGB_8888; int bitmapByteSize = Util.getBitmapByteSize(dimensions, dimensions, config); PreFillType preFillType = new PreFillType.Builder(dimensions).setConfig(config).build(); Map allocationOrder = new HashMap<>(); allocationOrder.put(preFillType, 1); PreFillQueue queue = new PreFillQueue(allocationOrder); BitmapPreFillRunner runner = new BitmapPreFillRunner( new LruBitmapPool(bitmapByteSize - 1), new MemoryCacheAdapter(), queue); runner.allocate(); } private static final class AddBitmapPoolAnswer implements Answer { private final List bitmaps; AddBitmapPoolAnswer(List bitmaps) { this.bitmaps = bitmaps; } @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { Bitmap bitmap = (Bitmap) invocationOnMock.getArguments()[0]; bitmaps.add(bitmap); return null; } } private static final class AddBitmapCacheAnswer implements Answer> { private final List bitmaps; AddBitmapCacheAnswer(List bitmaps) { this.bitmaps = bitmaps; } @Override public Resource answer(InvocationOnMock invocationOnMock) throws Throwable { BitmapResource resource = (BitmapResource) invocationOnMock.getArguments()[1]; bitmaps.add(resource.get()); return null; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillerTest.java ================================================ package com.bumptech.glide.load.engine.prefill; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.cache.MemoryCache; import com.bumptech.glide.tests.Util.CreateBitmap; import com.bumptech.glide.util.Util; import com.google.common.collect.Range; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapPreFillerTest { private static final int DEFAULT_BITMAP_WIDTH = 100; private static final int DEFAULT_BITMAP_HEIGHT = 50; private static final int BITMAPS_IN_POOL = 10; private static final int BITMAPS_IN_CACHE = 10; private final Bitmap.Config defaultBitmapConfig = PreFillType.DEFAULT_CONFIG; private final Bitmap defaultBitmap = Bitmap.createBitmap(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT, defaultBitmapConfig); private final long defaultBitmapSize = Util.getBitmapByteSize(defaultBitmap); private final long poolSize = BITMAPS_IN_CACHE * defaultBitmapSize; private final long cacheSize = BITMAPS_IN_POOL * defaultBitmapSize; @Mock private BitmapPool pool; @Mock private MemoryCache cache; private BitmapPreFiller bitmapPreFiller; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(pool.getMaxSize()).thenReturn(poolSize); when(pool.getDirty(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenAnswer(new CreateBitmap()); when(cache.getMaxSize()).thenReturn(cacheSize); bitmapPreFiller = new BitmapPreFiller(cache, pool, DecodeFormat.DEFAULT); } @Test public void testAllocationOrderContainsEnoughSizesToFillPoolAndMemoryCache() { PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder( new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build()); assertEquals(BITMAPS_IN_POOL + BITMAPS_IN_CACHE, allocationOrder.getSize()); } @Test public void testAllocationOrderThatDoesNotFitExactlyIntoGivenSizeRoundsDown() { PreFillType[] sizes = new PreFillType[] { new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(), new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(), new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2) .setConfig(defaultBitmapConfig) .build(), }; PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(sizes); int byteSize = 0; while (!allocationOrder.isEmpty()) { PreFillType current = allocationOrder.remove(); byteSize += Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig()); } int expectedSize = 0; long maxSize = poolSize + cacheSize; for (PreFillType current : sizes) { int currentSize = Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig()); // See https://errorprone.info/bugpattern/NarrowingCompoundAssignment. expectedSize = (int) (expectedSize + (currentSize * (maxSize / (3 * currentSize)))); } assertEquals(expectedSize, byteSize); } @Test public void testAllocationOrderDoesNotOverFillWithMultipleSizes() { PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder( new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(), new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(), new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2) .setConfig(defaultBitmapConfig) .build()); long byteSize = 0; while (!allocationOrder.isEmpty()) { PreFillType current = allocationOrder.remove(); byteSize += Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig()); } assertThat(byteSize).isIn(Range.atMost(poolSize + cacheSize)); } @Test public void testAllocationOrderDoesNotOverFillWithMultipleSizesAndWeights() { PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder( new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .setWeight(4) .build(), new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(), new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 3) .setConfig(defaultBitmapConfig) .setWeight(3) .build()); long byteSize = 0; while (!allocationOrder.isEmpty()) { PreFillType current = allocationOrder.remove(); byteSize += Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig()); } assertThat(byteSize).isIn(Range.atMost(poolSize + cacheSize)); } @Test public void testAllocationOrderContainsSingleSizeIfSingleSizeIsProvided() { PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder( new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build()); while (!allocationOrder.isEmpty()) { PreFillType size = allocationOrder.remove(); assertEquals(DEFAULT_BITMAP_WIDTH, size.getWidth()); assertEquals(DEFAULT_BITMAP_HEIGHT, size.getHeight()); assertEquals(defaultBitmapConfig, size.getConfig()); } } @Test public void testAllocationOrderSplitsEvenlyBetweenEqualSizesWithEqualWeights() { PreFillType smallWidth = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(); PreFillType smallHeight = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2) .setConfig(defaultBitmapConfig) .build(); PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(smallWidth, smallHeight); int numSmallWidth = 0; int numSmallHeight = 0; while (!allocationOrder.isEmpty()) { PreFillType current = allocationOrder.remove(); if (smallWidth.equals(current)) { numSmallWidth++; } else if (smallHeight.equals(current)) { numSmallHeight++; } else { fail("Unexpected size, size: " + current); } } assertEquals(numSmallWidth, numSmallHeight); } @Test public void testAllocationOrderSplitsByteSizeEvenlyBetweenUnEqualSizesWithEqualWeights() { PreFillType smallWidth = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(); PreFillType normal = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(); PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(smallWidth, normal); int numSmallWidth = 0; int numNormal = 0; while (!allocationOrder.isEmpty()) { PreFillType current = allocationOrder.remove(); if (smallWidth.equals(current)) { numSmallWidth++; } else if (normal.equals(current)) { numNormal++; } else { fail("Unexpected size, size: " + current); } } assertEquals(2 * numNormal, numSmallWidth); } @Test public void testAllocationOrderSplitsByteSizeUnevenlyBetweenEqualSizesWithUnequalWeights() { PreFillType doubleWeight = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .setWeight(2) .build(); PreFillType normal = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2) .setConfig(defaultBitmapConfig) .build(); PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(doubleWeight, normal); int numDoubleWeight = 0; int numNormal = 0; while (!allocationOrder.isEmpty()) { PreFillType current = allocationOrder.remove(); if (doubleWeight.equals(current)) { numDoubleWeight++; } else if (normal.equals(current)) { numNormal++; } else { fail("Unexpected size, size: " + current); } } assertEquals(2 * numNormal, numDoubleWeight); } @Test public void testAllocationOrderRoundRobinsDifferentSizes() { when(pool.getMaxSize()).thenReturn(defaultBitmapSize); when(cache.getMaxSize()).thenReturn(defaultBitmapSize); PreFillType smallWidth = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT) .setConfig(defaultBitmapConfig) .build(); PreFillType smallHeight = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2) .setConfig(defaultBitmapConfig) .build(); PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(smallWidth, smallHeight); List attributes = new ArrayList<>(); while (!allocationOrder.isEmpty()) { attributes.add(allocationOrder.remove()); } // Either width, height, width, height or height, width, height, width. try { assertThat(attributes) .containsExactly(smallWidth, smallHeight, smallWidth, smallHeight) .inOrder(); } catch (AssertionError e) { assertThat(attributes) .containsExactly(smallHeight, smallWidth, smallHeight, smallWidth) .inOrder(); } } @Test @SuppressWarnings("deprecation") public void testSetsConfigOnBuildersToDefaultIfNotSet() { PreFillType.Builder builder = mock(PreFillType.Builder.class); when(builder.build()) .thenReturn(new PreFillType.Builder(100).setConfig(Bitmap.Config.RGB_565).build()); bitmapPreFiller.preFill(builder); InOrder order = inOrder(builder); order .verify(builder) .setConfig( DecodeFormat.DEFAULT == DecodeFormat.PREFER_ARGB_8888 ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); order.verify(builder).build(); } @Test public void testDoesNotSetConfigOnBuildersIfConfigIsAlreadySet() { PreFillType.Builder builder = mock(PreFillType.Builder.class); when(builder.getConfig()).thenReturn(Bitmap.Config.ARGB_4444); when(builder.build()) .thenReturn(new PreFillType.Builder(100).setConfig(Bitmap.Config.ARGB_4444).build()); bitmapPreFiller.preFill(builder); verify(builder, never()).setConfig(any(Bitmap.Config.class)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/engine/prefill/PreFillTypeTest.java ================================================ package com.bumptech.glide.load.engine.prefill; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import android.graphics.Bitmap; import com.google.common.testing.EqualsTester; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class PreFillTypeTest { @Test(expected = IllegalArgumentException.class) public void testThrowsIfSizeIsZero() { new PreFillType.Builder(0); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfWidthIsZero() { new PreFillType.Builder(0, 100); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfHeightIsZero() { new PreFillType.Builder(100, 0); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfWeightIsZero() { new PreFillType.Builder(100).setWeight(0); } @Test(expected = NullPointerException.class) public void testConstructorThrowsIfConfigIsNull() { new PreFillType(100, 100, null, 1); } @Test public void testGetWidthReturnsGivenWidth() { int width = 500; assertEquals(width, new PreFillType(width, 100, Bitmap.Config.ARGB_4444, 1).getWidth()); } @Test public void testGetHeightReturnsGivenHeight() { int height = 123; assertEquals(height, new PreFillType(100, height, Bitmap.Config.ARGB_4444, 1).getHeight()); } @Test public void testGetConfigReturnsGivenConfig() { Bitmap.Config config = Bitmap.Config.ARGB_8888; assertEquals(config, new PreFillType(100, 100, config, 1).getConfig()); } @Test public void testGetWeightReturnsGivenWeight() { int weight = 400; assertEquals(weight, new PreFillType(100, 100, Bitmap.Config.ARGB_4444, weight).getWeight()); } @Test public void testEquality() { new EqualsTester() .addEqualityGroup( new PreFillType(100, 100, Bitmap.Config.ARGB_4444, 1), new PreFillType(100, 100, Bitmap.Config.ARGB_4444, 1)) .addEqualityGroup(new PreFillType(200, 100, Bitmap.Config.ARGB_4444, 1)) .addEqualityGroup(new PreFillType(100, 200, Bitmap.Config.ARGB_4444, 1)) .addEqualityGroup(new PreFillType(100, 100, Bitmap.Config.ARGB_8888, 1)) .addEqualityGroup(new PreFillType(100, 100, Bitmap.Config.ARGB_4444, 2)) .testEquals(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/AssetUriLoaderTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.content.res.AssetManager; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.util.Preconditions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class AssetUriLoaderTest { private static final int IMAGE_SIDE = 10; @Mock private AssetUriLoader.AssetFetcherFactory factory; @Mock private DataFetcher fetcher; private AssetUriLoader loader; @Before public void setUp() { MockitoAnnotations.initMocks(this); loader = new AssetUriLoader<>(ApplicationProvider.getApplicationContext().getAssets(), factory); } @Test public void testHandlesAssetUris() { Uri assetUri = Uri.parse("file:///android_asset/assetName"); when(factory.buildFetcher(any(AssetManager.class), eq("assetName"))).thenReturn(fetcher); assertTrue(loader.handles(assetUri)); assertEquals( fetcher, Preconditions.checkNotNull( loader.buildLoadData(assetUri, IMAGE_SIDE, IMAGE_SIDE, new Options())) .fetcher); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/ByteArrayLoaderTest.java ================================================ package com.bumptech.glide.load.model; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.bumptech.glide.Priority; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(JUnit4.class) public class ByteArrayLoaderTest { @Mock private ByteArrayLoader.Converter converter; @Mock private DataFetcher.DataCallback callback; private ByteArrayLoader loader; private Options options; @Before public void setUp() { MockitoAnnotations.initMocks(this); loader = new ByteArrayLoader<>(converter); options = new Options(); } @Test public void testCanHandleByteArray() { byte[] data = new byte[10]; DataFetcher fetcher = Preconditions.checkNotNull(loader.buildLoadData(data, -1, -1, options)).fetcher; assertNotNull(fetcher); } @Test public void testFetcherReturnsObjectReceivedFromConverter() throws IOException { byte[] data = "fake".getBytes("UTF-8"); Object expected = new Object(); when(converter.convert(eq(data))).thenReturn(expected); Preconditions.checkNotNull(loader.buildLoadData(data, 10, 10, options)) .fetcher .loadData(Priority.HIGH, callback); verify(callback).onDataReady(eq(expected)); } @Test public void testFetcherReturnsDataClassFromConverter() { when(converter.getDataClass()).thenReturn(Object.class); assertEquals( Object.class, Preconditions.checkNotNull(loader.buildLoadData(new byte[10], 10, 10, options)) .fetcher .getDataClass()); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/DataUrlLoaderTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; /** Tests for the {@link DataUrlLoader} class. */ @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class DataUrlLoaderTest { // A valid base64-encoded PNG (a small "Google" logo). @SuppressWarnings("SpellCheckingInspection") private static final String VALID_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAALCAYAAAAeEY8BAAADFElEQVR42mNgAAK5ig+Cii" + "UfSmUL3mVL5r7PE8t5M1U06027eMYLMQZKQUMDE8eyxGrOJYmdDKtCmTHkFfO/iCsUfTykUPFeASH6n1Es+3Wj" + "SM5rKQYqANbFcTmsC2OXYpWUKXw/R67ofQEhQ+5FecnfDnYxPJNmzAp35n8Gxv/7pTT+75PQBrFh4iq5b/lk8z" + "+aiue+tZDKeaPBMC8qh2leFNgB/xkYGO+Eu+ncCnZRAiuWyHv3VDzngxMui0EWPgpx6n4U4Wx7J8De86aP2blr" + "rgaq//fwCv8/KNT//5CU0f99okn/dwse+b9fQECx9IObQvGHMrn8D66See9eiWa9s2GYE57DMCdi6Qs3N+6HIc" + "4T70a4mtz2t55909u0jkE85+1Tsdx30ciWSuQ+F+VPe6kskPFc4Z6XRcp9H8t2mNxVF72Gq066K//vZe//v4cD" + "ru//ds7V/7dx1MoXf9gtW/zRFGLO+x7x7DeVDDOBDpgZvvSut3nWXR/LyptuxgG33Axzr7rr2TKIZb1eIpL1ej" + "co3mGGCWe8cRJMf7FVKO1F/y1Xww4gng6Tu+Ko7X7JTvPo/52Mm//vYMqBO2AbU/H/LUwzpQreT5LOf98PEhPL" + "ftslkfvGjGF6aA4QL73halh7y9XgwHVnM2G4b0G+FM549Uw440U7Q+h/eCoVSH0+GYjrrjrr2V530n16w1qdFy" + "R+wUYr6YKNRtH/7QzpQHzsfwMDE9gBmxl6/29hcNdu+M8G9HmCWM7bQ6I5bxPBhk0NzmGYErT0mpOe0TVHnY+X" + "HXRMQMKrQhkg9omkvZYUSHvZJ5T+Yh3IUoHUZ/mCqc87BdOe2UB9HXzZQWvCeTuNqPO2GgmghAROgFsZ8oCWtg" + "BxDNABASC1olmveEQyX/sB8SKRzJcPgbQxw0S/IoaJvksZJvsqXnLQDLhoq7n7nI3GxHOWWs4M1AQ8ic9FhdNf" + "7ZRKeyYCjsrUly7AqDzOQC8glP7SFWjhCVhUKiTc5xBIebaAbg4AWcyf+qxNMPXZKoGU57UCqU+KQKGCTwsAbx" + "BBmvLaD+cAAAAASUVORK5CYII="; private static final String INVALID_URL_WRONG_SCHEME1 = "test"; private static final String INVALID_URL_WRONG_SCHEME2 = "http://google.com"; private static final String INVALID_URL_WRONG_SCHEME3 = "data:text"; private static final String INVALID_URL_MISSING_COMMA = "data:image/png;base64=NOT_BASE64"; private static final String INVALID_URL_WRONG_ENCODING = "data:image/png;base32,"; @Mock private MultiModelLoaderFactory multiFactory; private DataUrlLoader dataUrlLoader; private DataFetcher fetcher; private Options options; @Before public void setUp() { MockitoAnnotations.initMocks(this); DataUrlLoader.StreamFactory factory = new DataUrlLoader.StreamFactory<>(); options = new Options(); dataUrlLoader = (DataUrlLoader) factory.build(multiFactory); fetcher = dataUrlLoader.buildLoadData(VALID_PNG, -1, -1, options).fetcher; } @Test public void testHandleDataUri() { assertTrue(dataUrlLoader.handles(VALID_PNG)); } @Test public void testHandleFalseDataUri() { assertFalse(dataUrlLoader.handles(INVALID_URL_WRONG_SCHEME1)); assertFalse(dataUrlLoader.handles(INVALID_URL_WRONG_SCHEME2)); assertFalse(dataUrlLoader.handles(INVALID_URL_WRONG_SCHEME3)); } @Test public void testDecode() throws IOException { byte[] expected = Base64.decode(VALID_PNG.substring(VALID_PNG.indexOf(',') + 1), Base64.DEFAULT); CallBack callback = new CallBack(); fetcher.loadData(Priority.HIGH, callback); byte[] result = new byte[((ByteArrayInputStream) callback.data).available()]; assertEquals(result.length, ((ByteArrayInputStream) callback.data).read(result)); assertTrue(Arrays.equals(result, expected)); assertNull(callback.exception); } @Test public void testDecodeInvalidScheme() { fetcher = dataUrlLoader.buildLoadData(INVALID_URL_WRONG_SCHEME1, -1, -1, options).fetcher; CallBack callback = new CallBack(); fetcher.loadData(Priority.HIGH, callback); assertNotNull(callback.exception); } @Test public void testDecodeMissingComma() { fetcher = dataUrlLoader.buildLoadData(INVALID_URL_MISSING_COMMA, -1, -1, options).fetcher; CallBack callback = new CallBack(); fetcher.loadData(Priority.HIGH, callback); assertNotNull(callback.exception); } @Test public void testDecodeWrongEncoding() { fetcher = dataUrlLoader.buildLoadData(INVALID_URL_WRONG_ENCODING, -1, -1, options).fetcher; CallBack callback = new CallBack(); fetcher.loadData(Priority.HIGH, callback); assertNotNull(callback.exception); } private static final class CallBack implements DataFetcher.DataCallback { public Object data; public Exception exception; @Override public void onDataReady(@Nullable Object data) { this.data = data; } @Override public void onLoadFailed(@NonNull Exception e) { this.exception = e; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/GlideUrlTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import com.google.common.testing.EqualsTester; import java.net.MalformedURLException; import java.net.URL; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GlideUrlTest { @Test(expected = NullPointerException.class) public void testThrowsIfGivenURLIsNull() { new GlideUrl((URL) null); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfGivenStringUrlIsNull() { new GlideUrl((String) null); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfGivenStringURLIsEmpty() { new GlideUrl(""); } @Test public void testCanCompareGlideUrlsCreatedWithDifferentTypes() throws MalformedURLException { String stringUrl = "http://www.google.com"; URL url = new URL(stringUrl); assertEquals(new GlideUrl(stringUrl), new GlideUrl(url)); } @Test public void testCanCompareHashcodeOfGlideUrlsCreatedWithDifferentTypes() throws MalformedURLException { String stringUrl = "http://nytimes.com"; URL url = new URL(stringUrl); assertEquals(new GlideUrl(stringUrl).hashCode(), new GlideUrl(url).hashCode()); } @Test public void testProducesEquivalentUrlFromString() throws MalformedURLException { String stringUrl = "http://www.google.com"; GlideUrl glideUrl = new GlideUrl(stringUrl); URL url = glideUrl.toURL(); assertEquals(stringUrl, url.toString()); } @Test public void testProducesEquivalentStringFromURL() throws MalformedURLException { String expected = "http://www.washingtonpost.com"; URL url = new URL(expected); GlideUrl glideUrl = new GlideUrl(url); assertEquals(expected, glideUrl.toStringUrl()); } @Test public void testIssue133() throws MalformedURLException { // u00e0=à final String original = "http://www.commitstrip.com/wp-content/uploads/2014/07/" + "Excel-\u00E0-toutes-les-sauces-650-finalenglish.jpg"; final String escaped = "http://www.commitstrip.com/wp-content/uploads/2014/07/" + "Excel-%C3%A0-toutes-les-sauces-650-finalenglish.jpg"; GlideUrl glideUrlFromString = new GlideUrl(original); assertEquals(escaped, glideUrlFromString.toURL().toString()); GlideUrl glideUrlFromEscapedString = new GlideUrl(escaped); assertEquals(escaped, glideUrlFromEscapedString.toURL().toString()); GlideUrl glideUrlFromUrl = new GlideUrl(new URL(original)); assertEquals(escaped, glideUrlFromUrl.toURL().toString()); GlideUrl glideUrlFromEscapedUrl = new GlideUrl(new URL(escaped)); assertEquals(escaped, glideUrlFromEscapedUrl.toURL().toString()); } @Test public void issue_2583() throws MalformedURLException { String original = "http://api.met.no/weatherapi/weathericon/1.1/?symbol=9;content_type=image/png"; GlideUrl glideUrl = new GlideUrl(original); assertThat(glideUrl.toURL().toString()).isEqualTo(original); assertThat(glideUrl.toStringUrl()).isEqualTo(original); } @Test public void testEquals() throws MalformedURLException { Headers headers = mock(Headers.class); Headers otherHeaders = mock(Headers.class); String url = "http://www.google.com"; String otherUrl = "http://mail.google.com"; new EqualsTester() .addEqualityGroup( new GlideUrl(url), new GlideUrl(url), new GlideUrl(new URL(url)), new GlideUrl(new URL(url))) .addEqualityGroup(new GlideUrl(otherUrl), new GlideUrl(new URL(otherUrl))) .addEqualityGroup(new GlideUrl(url, headers), new GlideUrl(new URL(url), headers)) .addEqualityGroup(new GlideUrl(url, otherHeaders), new GlideUrl(new URL(url), otherHeaders)) .testEquals(); } @Test public void issue_5444() throws MalformedURLException { String original = "http://[2600:1f13:37c:1400:ba21:7165:5fc7:736e]/"; GlideUrl glideUrl = new GlideUrl(original); assertThat(glideUrl.toURL().toString()).isEqualTo(original); assertThat(glideUrl.toStringUrl()).isEqualTo(original); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/LazyHeadersTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import androidx.annotation.Nullable; import com.bumptech.glide.load.model.LazyHeaders.Builder; import com.google.common.testing.EqualsTester; import java.util.Map; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class LazyHeadersTest { private static final String DEFAULT_USER_AGENT = "default_user_agent"; private static final String DEFAULT_USER_AGENT_PROPERTY = "http.agent"; private String initialUserAgent; @Before public void setUp() { initialUserAgent = System.getProperty(DEFAULT_USER_AGENT_PROPERTY); System.setProperty(DEFAULT_USER_AGENT_PROPERTY, DEFAULT_USER_AGENT); } @After public void tearDown() { if (initialUserAgent != null) { System.setProperty(DEFAULT_USER_AGENT_PROPERTY, initialUserAgent); } } // Tests for #2331. @Test public void getSanitizedUserAgent_withInvalidAgent_returnsAgentWithInvalidCharactersRemoved() { String invalidUserAgent = "Dalvik/2.1.0 (Linux; U; Android 5.0; P98 4G八核版(A8H8) Build/LRX21M)"; String validUserAgent = "Dalvik/2.1.0 (Linux; U; Android 5.0; P98 4G???(A8H8) Build/LRX21M)"; System.setProperty(DEFAULT_USER_AGENT_PROPERTY, invalidUserAgent); assertThat(LazyHeaders.Builder.getSanitizedUserAgent()).isEqualTo(validUserAgent); } @Test public void getSanitizedUserAgent_withValidAgent_returnsUnmodifiedAgent() { String validUserAgent = "Dalvik/2.1.0 (Linux; U; Android 5.0; P98 4G(A8H8) Build/LRX21M)"; System.setProperty(DEFAULT_USER_AGENT_PROPERTY, validUserAgent); assertThat(LazyHeaders.Builder.getSanitizedUserAgent()).isEqualTo(validUserAgent); } @Test public void getSanitizedUserAgent_withMissingAgent_returnsNull() { System.clearProperty(DEFAULT_USER_AGENT_PROPERTY); assertThat(LazyHeaders.Builder.getSanitizedUserAgent()).isNull(); } @Test public void getSanitizedUserAgent_withEmptyStringAgent_returnsEmptyString() { String userAgent = ""; System.setProperty(DEFAULT_USER_AGENT_PROPERTY, userAgent); assertThat(LazyHeaders.Builder.getSanitizedUserAgent()).isEqualTo(userAgent); } @Test public void getSanitizedUserAgent_withWhitespace_returnsWhitespaceString() { String userAgent = " \t"; System.setProperty(DEFAULT_USER_AGENT_PROPERTY, userAgent); assertThat(LazyHeaders.Builder.getSanitizedUserAgent()).isEqualTo(userAgent); } @Test public void testIncludesEagerHeaders() { Map headers = new Builder().addHeader("key", "value").build().getHeaders(); assertThat(headers).containsEntry("key", "value"); } @Test public void testIncludesLazyHeaders() { LazyHeaderFactory factory = mock(LazyHeaderFactory.class); when(factory.buildHeader()).thenReturn("value"); Map headers = new Builder().addHeader("key", factory).build().getHeaders(); assertThat(headers).containsEntry("key", "value"); } @Test public void testMultipleEagerValuesAreSeparatedByCommas() { Map headers = new Builder().addHeader("key", "first").addHeader("key", "second").build().getHeaders(); assertThat(headers).containsEntry("key", "first,second"); } @Test public void testMultipleLazyValuesAreSeparatedByCommas() { LazyHeaderFactory first = mock(LazyHeaderFactory.class); when(first.buildHeader()).thenReturn("first"); LazyHeaderFactory second = mock(LazyHeaderFactory.class); when(second.buildHeader()).thenReturn("second"); Map headers = new Builder().addHeader("key", first).addHeader("key", second).build().getHeaders(); assertThat(headers).containsEntry("key", "first,second"); } @Test public void testMixedEagerAndLazyValuesAreIncluded() { LazyHeaderFactory factory = mock(LazyHeaderFactory.class); when(factory.buildHeader()).thenReturn("first"); Map headers = new Builder().addHeader("key", factory).addHeader("key", "second").build().getHeaders(); assertThat(headers).containsEntry("key", "first,second"); headers = new Builder().addHeader("key", "second").addHeader("key", factory).build().getHeaders(); assertThat(headers).containsEntry("key", "second,first"); } @Test public void testCanAddMultipleKeys() { LazyHeaderFactory factory = mock(LazyHeaderFactory.class); when(factory.buildHeader()).thenReturn("lazy"); Map headers = new Builder().addHeader("first", factory).addHeader("second", "eager").build().getHeaders(); assertThat(headers).containsEntry("first", "lazy"); assertThat(headers).containsEntry("second", "eager"); } @Test public void testUpdatingBuilderAfterBuildingDoesNotModifyOriginalHeaders() { Builder builder = new Builder(); builder.addHeader("key", "firstValue"); builder.addHeader("otherKey", "otherValue"); LazyHeaders first = builder.build(); LazyHeaderFactory factory = mock(LazyHeaderFactory.class); when(factory.buildHeader()).thenReturn("otherValue"); builder.addHeader("key", "secondValue"); builder.setHeader("otherKey", factory); LazyHeaders second = builder.build(); assertThat(first.getHeaders()).isNotEqualTo(second.getHeaders()); assertThat(first.getHeaders()).containsEntry("key", "firstValue"); assertThat(first.getHeaders()).containsEntry("otherKey", "otherValue"); assertThat(second.getHeaders()).containsEntry("key", "firstValue,secondValue"); assertThat(second.getHeaders()).containsEntry("otherKey", "otherValue"); } @Test public void testSetHeaderReplacesExistingHeaders() { Builder builder = new Builder(); builder.addHeader("key", "first").addHeader("key", "second").setHeader("key", "third"); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("key", "third"); } @Test public void testSetHeaderWithNullStringRemovesExistingHeader() { Builder builder = new Builder(); builder.addHeader("key", "first").addHeader("key", "second").setHeader("key", (String) null); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).doesNotContainKey("key"); } @Test public void testSetHeaderWithNullLazyHeaderFactoryRemovesExistingHeader() { Builder builder = new Builder(); builder .addHeader("key", "first") .addHeader("key", "second") .setHeader("key", (LazyHeaderFactory) null); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).doesNotContainKey("key"); } @Test public void testAddingEncodingHeaderReplacesDefaultThenAppends() { Builder builder = new Builder(); builder.addHeader("Accept-Encoding", "false"); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("Accept-Encoding", "false"); builder.addHeader("Accept-Encoding", "true"); headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("Accept-Encoding", "false,true"); } @Test public void testRemovingAndAddingEncodingHeaderReplacesDefaultThenAppends() { Builder builder = new Builder(); builder.setHeader("Accept-Encoding", (String) null); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).doesNotContainKey("Accept-Encoding"); builder.addHeader("Accept-Encoding", "false"); headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("Accept-Encoding", "false"); builder.addHeader("Accept-Encoding", "true"); headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("Accept-Encoding", "false,true"); } @Test public void testAddingUserAgentHeaderReplacesDefaultThenAppends() { Builder builder = new Builder(); builder.addHeader("User-Agent", "false"); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("User-Agent", "false"); builder.addHeader("User-Agent", "true"); headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("User-Agent", "false,true"); } @Test public void testRemovingAndAddingUserAgentHeaderReplacesDefaultThenAppends() { Builder builder = new Builder(); builder.setHeader("User-Agent", (String) null); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).doesNotContainKey("User-Agent"); builder.addHeader("User-Agent", "false"); headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("User-Agent", "false"); builder.addHeader("User-Agent", "true"); headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("User-Agent", "false,true"); } @Test public void testKeyNotIncludedWithFactoryThatReturnsNullValue() { Builder builder = new Builder(); builder.setHeader( "test", new LazyHeaderFactory() { @Nullable @Override public String buildHeader() { return null; } }); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).doesNotContainKey("test"); } @Test public void testKeyNotIncludedWithFactoryThatReturnsEmptyValue() { Builder builder = new Builder(); builder.setHeader( "test", new LazyHeaderFactory() { @Nullable @Override public String buildHeader() { return ""; } }); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).doesNotContainKey("test"); } @Test public void testKeyIncludedWithOneFactoryThatReturnsNullAndOneFactoryThatDoesNotReturnNull() { Builder builder = new Builder(); builder.addHeader( "test", new LazyHeaderFactory() { @Nullable @Override public String buildHeader() { return null; } }); builder.addHeader( "test", new LazyHeaderFactory() { @Nullable @Override public String buildHeader() { return "value"; } }); LazyHeaders headers = builder.build(); assertThat(headers.getHeaders()).containsEntry("test", "value"); } @Test public void testEquals() { LazyHeaderFactory firstLazyFactory = mock(LazyHeaderFactory.class); LazyHeaderFactory secondLazyFactory = mock(LazyHeaderFactory.class); new EqualsTester() .addEqualityGroup(new Builder().build(), new Builder().build()) .addEqualityGroup( new Builder().addHeader("key", "value").build(), new Builder().addHeader("key", "value").build()) .addEqualityGroup(new Builder().addHeader("key", "value").addHeader("key", "value").build()) .addEqualityGroup( new Builder().addHeader("key", firstLazyFactory).build(), new Builder().addHeader("key", firstLazyFactory).build()) .addEqualityGroup( new Builder() .addHeader("key", firstLazyFactory) .addHeader("key", firstLazyFactory) .build()) .addEqualityGroup( new Builder() .addHeader("firstKey", "value") .addHeader("secondKey", firstLazyFactory) .build(), new Builder() .addHeader("secondKey", firstLazyFactory) .addHeader("firstKey", "value") .build()) .addEqualityGroup(new Builder().addHeader("key", "secondValue")) .addEqualityGroup(new Builder().addHeader("secondKey", "value")) .addEqualityGroup(new Builder().addHeader("key", secondLazyFactory)) .addEqualityGroup(new Builder().addHeader("secondKey", firstLazyFactory)) .addEqualityGroup( new Builder() .addHeader("firstKey", "firstValue") .addHeader("secondKey", "secondValue") .build(), new Builder() .addHeader("firstKey", "firstValue") .addHeader("secondKey", "secondValue") .build(), new Builder() .addHeader("secondKey", "secondValue") .addHeader("firstKey", "firstValue") .build()) .addEqualityGroup( new Builder() .addHeader("firstKey", firstLazyFactory) .addHeader("secondKey", secondLazyFactory) .build(), new Builder() .addHeader("firstKey", firstLazyFactory) .addHeader("secondKey", secondLazyFactory) .build(), new Builder() .addHeader("secondKey", secondLazyFactory) .addHeader("firstKey", firstLazyFactory) .build()) .testEquals(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/ModelCacheTest.java ================================================ package com.bumptech.glide.load.model; import static org.junit.Assert.assertEquals; import com.google.common.testing.EqualsTester; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ModelCacheTest { private ModelCache cache; @Before public void setUp() { cache = new ModelCache<>(10); } @Test public void testModelKeyEquivalence() { new EqualsTester() .addEqualityGroup( ModelCache.ModelKey.get(14f, 100, 200), ModelCache.ModelKey.get(14f, 100, 200)) .addEqualityGroup(ModelCache.ModelKey.get(13f, 100, 200)) .addEqualityGroup(ModelCache.ModelKey.get(14f, 200, 200)) .addEqualityGroup(ModelCache.ModelKey.get(14f, 100, 300)) .testEquals(); } @Test public void testCanSetAndGetModel() { Object model = new Object(); int width = 10; int height = 20; Object result = new Object(); cache.put(model, width, height, result); assertEquals(result, cache.get(model, width, height)); } @Test public void testCanSetAndGetMultipleResultsWithDifferentDimensionsForSameObject() { Object model = new Object(); int firstWidth = 10; int firstHeight = 20; Object firstResult = new Object(); int secondWidth = 30; int secondHeight = 40; Object secondResult = new Object(); cache.put(model, firstWidth, firstHeight, firstResult); cache.put(model, secondWidth, secondHeight, secondResult); assertEquals(firstResult, cache.get(model, firstWidth, firstHeight)); assertEquals(secondResult, cache.get(model, secondWidth, secondHeight)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/ModelLoaderRegistryTest.java ================================================ package com.bumptech.glide.load.model; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import androidx.annotation.NonNull; import com.bumptech.glide.Registry.NoModelLoaderAvailableException; import com.bumptech.glide.util.pool.FactoryPools; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class ModelLoaderRegistryTest { private static final String MOCK_MODEL_LOADER_NAME = "MockModelLoader"; private ModelLoaderRegistry registry; @Before public void setUp() { registry = new ModelLoaderRegistry(FactoryPools.threadSafeList()); } @Test public void getModelLoaders_withNoRegisteredModelLoader_throws() { final Object model = new Object(); NoModelLoaderAvailableException thrown = assertThrows( NoModelLoaderAvailableException.class, new ThrowingRunnable() { @Override public void run() { registry.getModelLoaders(model); } }); assertThat(thrown) .hasMessageThat() .contains( "Failed to find any ModelLoaders registered for model class: " + model.getClass()); } @Test public void getModelLoaders_withRegisteredModelLoader_thatDoesNotHandleModelInstance_throws() { final Object model = new Object(); final ModelLoader modelLoader = mockModelLoader(); when(modelLoader.handles(model)).thenReturn(false); appendModelLoader(modelLoader); NoModelLoaderAvailableException thrown = assertThrows( NoModelLoaderAvailableException.class, new ThrowingRunnable() { @Override public void run() { registry.getModelLoaders(model); } }); assertThat(thrown) .hasMessageThat() .contains( "Found ModelLoaders for model class: [MockModelLoader], but none that handle this" + " specific model instance: java.lang.Object"); } @Test public void getModelLoaders_withRegisteredModelLoader_handlesModel_returnsModelLoader() { final Object model = new Object(); final ModelLoader modelLoader = mockModelLoader(); when(modelLoader.handles(model)).thenReturn(true); appendModelLoader(modelLoader); assertThat(registry.getModelLoaders(model)).containsExactly(modelLoader); } @Test public void getModelLoaders_withRegisteredModelLoaders_onlyOneHandlesModel_returnsHandlingModelLoader() { final Object model = new Object(); ModelLoader handlingModelLoader = mockModelLoader(); when(handlingModelLoader.handles(model)).thenReturn(true); appendModelLoader(handlingModelLoader); ModelLoader notHandlingModelLoader = mockModelLoader(); when(notHandlingModelLoader.handles(model)).thenReturn(false); appendModelLoader(notHandlingModelLoader); assertThat(registry.getModelLoaders(model)).containsExactly(handlingModelLoader); } private void appendModelLoader(final ModelLoader modelLoader) { registry.append( Object.class, Object.class, new ModelLoaderFactory() { @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return modelLoader; } @Override public void teardown() {} }); } @SuppressWarnings("unchecked") private static ModelLoader mockModelLoader() { return mock(ModelLoader.class, MOCK_MODEL_LOADER_NAME); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/MultiModelLoaderFactoryTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.core.util.Pools.Pool; import com.bumptech.glide.Registry.NoModelLoaderAvailableException; import com.bumptech.glide.tests.Util; import com.bumptech.glide.util.pool.FactoryPools; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; // containsExactly produces a spurious warning. @SuppressWarnings("ResultOfMethodCallIgnored") @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class MultiModelLoaderFactoryTest { @Mock private ModelLoaderFactory firstFactory; @Mock private ModelLoader firstModelLoader; @Mock private MultiModelLoaderFactory.Factory multiModelLoaderFactory; @Mock private ModelLoaderFactory secondFactory; @Mock private ModelLoader secondModelLoader; private Pool> throwableListPool; private MultiModelLoaderFactory multiFactory; @Before public void setUp() { MockitoAnnotations.initMocks(this); throwableListPool = FactoryPools.threadSafeList(); multiFactory = new MultiModelLoaderFactory(throwableListPool, multiModelLoaderFactory); when(firstFactory.build(eq(multiFactory))).thenReturn(firstModelLoader); when(secondFactory.build(eq(multiFactory))).thenReturn(secondModelLoader); } @Test public void testAppend_addsModelLoaderForModelClass() { multiFactory.append(String.class, String.class, firstFactory); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).containsExactly(firstModelLoader); } @Test public void testAppend_addsModelLoaderForModelAndDataClass() { multiFactory.append(String.class, String.class, firstFactory); ModelLoader modelLoader = multiFactory.build(String.class, String.class); assertThat(modelLoader).isEqualTo(firstModelLoader); } @Test public void testPrepend_addsModelLoaderForModelClass() { multiFactory.prepend(String.class, String.class, firstFactory); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).containsExactly(firstModelLoader); } @Test public void testPrepend_addsModelLoaderForModelAndDataClass() { multiFactory.prepend(String.class, String.class, firstFactory); ModelLoader modelLoader = multiFactory.build(String.class, String.class); assertThat(modelLoader).isEqualTo(firstModelLoader); } @Test public void testReplace_addsModelLoaderForModelClass() { multiFactory.replace(String.class, String.class, firstFactory); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).containsExactly(firstModelLoader); } @Test public void testReplace_addsModelLoaderForModelAndDataClasses() { multiFactory.replace(String.class, String.class, firstFactory); ModelLoader modelLoader = multiFactory.build(String.class, String.class); assertThat(modelLoader).isEqualTo(firstModelLoader); } @Test public void testReplace_returnsPreviouslyRegisteredFactories_withModelAndDataClasses() { ModelLoaderFactory firstOtherFactory = mockFactory(); ModelLoaderFactory secondOtherFactory = mockFactory(); multiFactory.append(String.class, String.class, firstOtherFactory); multiFactory.append(String.class, String.class, secondOtherFactory); List> removed = multiFactory.replace(String.class, String.class, firstFactory); assertThat(removed).containsExactly(firstOtherFactory, secondOtherFactory); } @Test public void testReplace_removesPreviouslyRegisteredFactories_withModelAndDataClasses() { appendFactoryFor(String.class, String.class); appendFactoryFor(String.class, String.class); multiFactory.replace(String.class, String.class, firstFactory); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).containsExactly(firstModelLoader); } @Test public void testRemove_returnsPreviouslyRegisteredFactories_withModelAndDataClasses() { ModelLoaderFactory other = mockFactory(); multiFactory.append(String.class, String.class, other); multiFactory.append(String.class, String.class, firstFactory); List> removed = multiFactory.remove(String.class, String.class); assertThat(removed).containsExactly(firstFactory, other); } @Test public void testRemove_removesPreviouslyRegisteredFactories_withModelAndDataClasses() { appendFactoryFor(String.class, String.class); appendFactoryFor(String.class, String.class); multiFactory.remove(String.class, String.class); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).isEmpty(); } @Test public void testBuild_withModelClass_returnsMultipleModelLoaders_ofGivenModelAndDataClasses() { ModelLoader otherLoader = appendFactoryFor(String.class, String.class); multiFactory.append(String.class, String.class, firstFactory); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).containsExactly(otherLoader, firstModelLoader); } @Test public void testBuild_withModelClass_returnsMultipleModelLoaders_ofGivenModelClassWithDifferentDataClasses() { ModelLoader otherLoader = appendFactoryFor(String.class, Integer.class); multiFactory.append(String.class, String.class, firstFactory); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).containsExactly(otherLoader, firstModelLoader); } @SuppressWarnings("TruthIncompatibleType") @Test public void testBuild_withModelClass_excludesModelLoadersForOtherModelClasses() { multiFactory.append(String.class, String.class, firstFactory); List> modelLoaders = multiFactory.build(Integer.class); assertThat(modelLoaders) .doesNotContain( /* expected: ModelLoader, actual: ModelLoader */ firstModelLoader); } @Test public void testBuild_withModelAndDataClasses_returnsMultipleModelLoaders_ofGivenModelAndDataClasses() { ModelLoader otherLoader = appendFactoryFor(String.class, String.class); multiFactory.append(String.class, String.class, firstFactory); List> modelLoaders = buildModelLoaders(String.class, String.class); assertThat(modelLoaders).containsExactly(otherLoader, firstModelLoader); } @Test public void testBuild_withModelAndDataClasses_excludesModelLoadersForOtherDataClasses() { multiFactory.append(String.class, String.class, firstFactory); assertThrows( NoModelLoaderAvailableException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { multiFactory.build(String.class, Integer.class); } }); } @Test public void testBuild_withModelAndDataClasses_excludesModelLoadersForOtherModelClasses() { multiFactory.append(String.class, String.class, firstFactory); assertThrows( NoModelLoaderAvailableException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { multiFactory.build(Integer.class, String.class); } }); } @Test public void testBuild_withModelClass_doesNotMatchSubclassesOfModelClass() { ModelLoader subclass = appendFactoryFor(String.class, Object.class); List> modelLoaders = multiFactory.build(Object.class); assertThat(modelLoaders).doesNotContain(subclass); } @Test public void testBuild_withModelClass_matchesSuperclassesOfModelClass() { ModelLoader superclass = appendFactoryFor(Object.class, Object.class); List> modelLoaders = multiFactory.build(String.class); assertThat(modelLoaders).contains(superclass); } @Test public void testBuild_withModelAndDataClass_doesNotMatchSubclassesOfModelClass() { appendFactoryFor(String.class, Object.class); assertThrows( NoModelLoaderAvailableException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { multiFactory.build(Object.class, Object.class); } }); } @Test public void testBuild_withModelAndDataClass_doesNotMatchSubclassesOfDataClass() { appendFactoryFor(Object.class, String.class); assertThrows( NoModelLoaderAvailableException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { multiFactory.build(Object.class, Object.class); } }); } @Test public void testBuild_withModelAndDataClass_doesMatchSuperclassesOfModelClass() { ModelLoader firstSuperClass = appendFactoryFor(Object.class, Object.class); ModelLoader secondSuperClass = appendFactoryFor(Object.class, Object.class); List> modelLoaders = buildModelLoaders(String.class, Object.class); assertThat(modelLoaders).containsExactly(firstSuperClass, secondSuperClass); } @Test public void testBuild_withModelAndDataClass_matchesSuperclassesOfDataClass() { ModelLoader firstSuperClass = appendFactoryFor(Object.class, Object.class); ModelLoader secondSuperClass = appendFactoryFor(Object.class, Object.class); List> modelLoaders = buildModelLoaders(Object.class, String.class); assertThat(modelLoaders).containsExactly(firstSuperClass, secondSuperClass); } @Test public void testBuild_withModelAndDataClass_matchesSuperclassOfModelAndDataClass() { ModelLoader firstSuperclass = appendFactoryFor(Object.class, Object.class); ModelLoader secondSuperclass = appendFactoryFor(Object.class, Object.class); List> modelLoaders = buildModelLoaders(String.class, String.class); assertThat(modelLoaders).containsExactly(firstSuperclass, secondSuperclass); } @Test public void testBuild_respectsAppendOrder() { ModelLoader first = appendFactoryFor(String.class, String.class); ModelLoader second = appendFactoryFor(String.class, String.class); ModelLoader third = appendFactoryFor(String.class, String.class); List> modelLoaders = buildModelLoaders(String.class, String.class); assertThat(modelLoaders).containsExactly(first, second, third).inOrder(); } @Test public void testBuild_respectsPrependOrder() { ModelLoader first = prependFactoryFor(String.class, String.class); ModelLoader second = prependFactoryFor(String.class, String.class); ModelLoader third = prependFactoryFor(String.class, String.class); List> modelLoaders = buildModelLoaders(String.class, String.class); assertThat(modelLoaders).containsExactly(third, second, first).inOrder(); } private List> buildModelLoaders( Class modelClass, Class dataClass) { ArgumentCaptor>> captor = Util.cast(ArgumentCaptor.forClass(List.class)); multiFactory.build(modelClass, dataClass); verify(multiModelLoaderFactory).build(captor.capture(), eq(throwableListPool)); List> captured = captor.getValue(); List> result = new ArrayList<>(captured.size()); result.addAll(captured); return result; } private ModelLoader appendFactoryFor(Class modelClass, Class dataClass) { return registerFactoryFor(modelClass, dataClass, true /*append*/); } private ModelLoader prependFactoryFor(Class modelClass, Class dataClass) { return registerFactoryFor(modelClass, dataClass, false /*append*/); } private ModelLoader registerFactoryFor( Class modelClass, Class dataClass, boolean append) { ModelLoaderFactory factory = mockFactory(); @SuppressWarnings("unchecked") ModelLoader loader = mock(ModelLoader.class); when(factory.build(eq(multiFactory))).thenReturn(loader); if (append) { multiFactory.append(modelClass, dataClass, factory); } else { multiFactory.prepend(modelClass, dataClass, factory); } return loader; } @SuppressWarnings("unchecked") private static ModelLoaderFactory mockFactory() { return mock(ModelLoaderFactory.class); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/ResourceLoaderTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.util.Preconditions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; /** Tests for the {@link com.bumptech.glide.load.model.ResourceLoader} class. */ @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ResourceLoaderTest { @Mock private ModelLoader uriLoader; @Mock private DataFetcher fetcher; @Mock private Key key; private Options options; private ResourceLoader loader; @Before public void setUp() { MockitoAnnotations.initMocks(this); options = new Options(); loader = new ResourceLoader<>(ApplicationProvider.getApplicationContext().getResources(), uriLoader); } @Test public void testCanHandleId() { int id = android.R.drawable.star_off; Uri contentUri = Uri.parse("android.resource://android/" + String.valueOf(id)); when(uriLoader.buildLoadData(eq(contentUri), anyInt(), anyInt(), any(Options.class))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(id)); assertEquals( fetcher, Preconditions.checkNotNull(loader.buildLoadData(id, 100, 100, new Options())).fetcher); } @Test public void testDoesNotThrowOnInvalidOrMissingId() { assertThat(loader.buildLoadData(1234, 0, 0, options)).isNull(); verify(uriLoader, never()) .buildLoadData(any(Uri.class), anyInt(), anyInt(), any(Options.class)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/StreamEncoderTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.util.ByteBufferUtil; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class StreamEncoderTest { private StreamEncoder encoder; private File file; @Before public void setUp() { encoder = new StreamEncoder(new LruArrayPool()); file = new File(ApplicationProvider.getApplicationContext().getCacheDir(), "test"); } @After public void tearDown() { // GC before delete() to release files on Windows (https://stackoverflow.com/a/4213208/253468) System.gc(); if (!file.delete()) { throw new IllegalStateException("Failed to delete: " + file); } } @Test public void testWritesDataFromInputStreamToOutputStream() throws IOException { String fakeData = "SomeRandomFakeData"; ByteArrayInputStream is = new ByteArrayInputStream(fakeData.getBytes("UTF-8")); encoder.encode(is, file, new Options()); byte[] data = ByteBufferUtil.toBytes(ByteBufferUtil.fromFile(file)); assertEquals(fakeData, new String(data, "UTF-8")); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/StringLoaderTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.tests.Util; import com.bumptech.glide.util.Preconditions; import java.io.File; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; /** Tests for the {@link com.bumptech.glide.load.model.StringLoader} class. */ @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class StringLoaderTest { // Not a magic number, just an arbitrary non zero value. private static final int IMAGE_SIDE = 100; @Mock private ModelLoader uriLoader; @Mock private DataFetcher fetcher; @Mock private Key key; private StringLoader loader; private Options options; @Before public void setUp() { MockitoAnnotations.initMocks(this); options = new Options(); when(uriLoader.handles(any(Uri.class))).thenReturn(true); loader = new StringLoader<>(uriLoader); } @Test public void testHandlesPaths() { // TODO fix drive letter parsing somehow assumeTrue("it will fail with schema being the drive letter (C:\\... -> C)", !Util.isWindows()); File f = ApplicationProvider.getApplicationContext().getCacheDir(); Uri expected = Uri.fromFile(f); when(uriLoader.buildLoadData(eq(expected), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(options))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(f.getAbsolutePath())); assertEquals( fetcher, Preconditions.checkNotNull( loader.buildLoadData(f.getAbsolutePath(), IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testCanHandleComplexFilePaths() { String testPath = "/storage/emulated/0/DCIM/Camera/IMG_20140520_100001:nopm:.jpg,mimeType=image/jpeg," + "2448x3264,orientation=0,date=Tue"; Uri expected = Uri.fromFile(new File(testPath)); when(uriLoader.buildLoadData(eq(expected), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(options))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(testPath)); assertEquals( fetcher, Preconditions.checkNotNull(loader.buildLoadData(testPath, IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testHandlesFileUris() { File f = ApplicationProvider.getApplicationContext().getCacheDir(); Uri expected = Uri.fromFile(f); when(uriLoader.buildLoadData(eq(expected), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(options))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(f.getAbsolutePath())); assertEquals( fetcher, Preconditions.checkNotNull( loader.buildLoadData(expected.toString(), IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testHandlesResourceUris() { Uri resourceUri = Uri.parse("android.resource://com.bumptech.glide.tests/raw/ic_launcher"); when(uriLoader.buildLoadData(eq(resourceUri), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(options))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(resourceUri.toString())); assertEquals( fetcher, Preconditions.checkNotNull( loader.buildLoadData(resourceUri.toString(), IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testHandlesHttp() { String url = "http://www.google.com"; Uri expected = Uri.parse(url); when(uriLoader.buildLoadData(eq(expected), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(options))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(url)); assertEquals( fetcher, Preconditions.checkNotNull(loader.buildLoadData(url, IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testHandlesHttps() { String url = "https://www.google.com"; Uri expected = Uri.parse(url); when(uriLoader.buildLoadData(eq(expected), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(options))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(url)); assertEquals( fetcher, Preconditions.checkNotNull(loader.buildLoadData(url, IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testHandlesContent() { String content = "content://com.bumptech.glide"; Uri expected = Uri.parse(content); when(uriLoader.buildLoadData(eq(expected), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(options))) .thenReturn(new ModelLoader.LoadData<>(key, fetcher)); assertTrue(loader.handles(content)); assertEquals( fetcher, Preconditions.checkNotNull(loader.buildLoadData(content, IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testGetResourceFetcher_withEmptyString_returnsNull() { assertThat(loader.buildLoadData("", IMAGE_SIDE, IMAGE_SIDE, options)).isNull(); assertThat(loader.buildLoadData(" ", IMAGE_SIDE, IMAGE_SIDE, options)).isNull(); assertThat(loader.buildLoadData(" \n", IMAGE_SIDE, IMAGE_SIDE, options)).isNull(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/UriLoaderTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.net.Uri; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.util.Preconditions; import java.io.File; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; /** Tests for the {@link UriLoader} class. */ @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class UriLoaderTest { // Not a magic number, just arbitrary non zero. private static final int IMAGE_SIDE = 120; @Mock private DataFetcher localUriFetcher; @Mock private UriLoader.LocalUriFetcherFactory factory; private UriLoader loader; private Options options; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); options = new Options(); loader = new UriLoader<>(factory); } @Test public void testHandlesFileUris() throws IOException { Uri fileUri = Uri.fromFile(new File("f")); when(factory.build(eq(fileUri))).thenReturn(localUriFetcher); assertTrue(loader.handles(fileUri)); assertEquals( localUriFetcher, Preconditions.checkNotNull(loader.buildLoadData(fileUri, IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } @Test public void testHandlesContentUris() { Uri contentUri = Uri.parse("content://com.bumptech.glide"); when(factory.build(eq(contentUri))).thenReturn(localUriFetcher); assertTrue(loader.handles(contentUri)); assertEquals( localUriFetcher, Preconditions.checkNotNull( loader.buildLoadData(contentUri, IMAGE_SIDE, IMAGE_SIDE, options)) .fetcher); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/UrlUriLoaderTest.java ================================================ package com.bumptech.glide.load.model; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import android.net.Uri; import com.bumptech.glide.load.Options; import java.io.InputStream; import java.net.MalformedURLException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class UrlUriLoaderTest { private static final int IMAGE_SIDE = 100; private static final Options OPTIONS = new Options(); @Mock private ModelLoader urlLoader; private UrlUriLoader loader; @Before public void setUp() { MockitoAnnotations.initMocks(this); loader = new UrlUriLoader<>(urlLoader); } @Test public void testHandlesHttpUris() throws MalformedURLException { Uri httpUri = Uri.parse("http://www.google.com"); loader.buildLoadData(httpUri, IMAGE_SIDE, IMAGE_SIDE, OPTIONS); assertTrue(loader.handles(httpUri)); verify(urlLoader) .buildLoadData( eq(new GlideUrl(httpUri.toString())), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(OPTIONS)); } @Test public void testHandlesHttpsUris() throws MalformedURLException { Uri httpsUri = Uri.parse("https://www.google.com"); loader.buildLoadData(httpsUri, IMAGE_SIDE, IMAGE_SIDE, OPTIONS); assertTrue(loader.handles(httpsUri)); verify(urlLoader) .buildLoadData( eq(new GlideUrl(httpsUri.toString())), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(OPTIONS)); } // Test for https://github.com/bumptech/glide/issues/71. @Test public void testHandlesMostlyInvalidHttpUris() { Uri mostlyInvalidHttpUri = Uri.parse( "http://myserver_url.com:80http://myserver_url.com/webapp/images/no_image.png" + "?size=100"); assertTrue(loader.handles(mostlyInvalidHttpUri)); loader.buildLoadData(mostlyInvalidHttpUri, IMAGE_SIDE, IMAGE_SIDE, OPTIONS); verify(urlLoader) .buildLoadData( eq(new GlideUrl(mostlyInvalidHttpUri.toString())), eq(IMAGE_SIDE), eq(IMAGE_SIDE), eq(OPTIONS)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/stream/BaseGlideUrlLoaderTest.java ================================================ package com.bumptech.glide.load.model.stream; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelCache; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.util.Preconditions; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BaseGlideUrlLoaderTest { @Mock private ModelCache modelCache; @Mock private ModelLoader wrapped; @Mock private DataFetcher fetcher; private TestLoader urlLoader; private Options options; @SuppressWarnings("unchecked") @Before public void setUp() { MockitoAnnotations.initMocks(this); options = new Options(); urlLoader = new TestLoader(wrapped, modelCache); } @Test public void testReturnsNullIfUrlIsNull() { urlLoader.resultUrl = null; assertNull(urlLoader.buildLoadData(new Object(), 100, 100, options)); } @Test public void testReturnsNullIfUrlIsEmpty() { urlLoader.resultUrl = " "; assertNull(urlLoader.buildLoadData(new Object(), 100, 100, options)); } @Test public void testReturnsUrlFromCacheIfPresent() { Object model = new Object(); int width = 100; int height = 200; GlideUrl expectedUrl = mock(GlideUrl.class); when(modelCache.get(eq(model), eq(width), eq(height))).thenReturn(expectedUrl); when(wrapped.buildLoadData(eq(expectedUrl), eq(width), eq(height), eq(options))) .thenReturn(new ModelLoader.LoadData<>(mock(Key.class), fetcher)); assertEquals( fetcher, Preconditions.checkNotNull(urlLoader.buildLoadData(model, width, height, options)).fetcher); } @Test public void testBuildsNewUrlIfNotPresentInCache() { int width = 10; int height = 11; urlLoader.resultUrl = "fakeUrl"; when(wrapped.buildLoadData(any(GlideUrl.class), eq(width), eq(height), eq(options))) .thenAnswer( new Answer>() { @Override public ModelLoader.LoadData answer(InvocationOnMock invocationOnMock) { GlideUrl glideUrl = (GlideUrl) invocationOnMock.getArguments()[0]; assertEquals(urlLoader.resultUrl, glideUrl.toStringUrl()); return new ModelLoader.LoadData<>(mock(Key.class), fetcher); } }); assertEquals( fetcher, Preconditions.checkNotNull( urlLoader.buildLoadData(new GlideUrl(urlLoader.resultUrl), width, height, options)) .fetcher); } @Test public void testAddsNewUrlToCacheIfNotPresentInCache() { urlLoader.resultUrl = "fakeUrl"; Object model = new Object(); int width = 400; int height = 500; doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { GlideUrl glideUrl = (GlideUrl) invocationOnMock.getArguments()[3]; assertEquals(urlLoader.resultUrl, glideUrl.toStringUrl()); return null; } }) .when(modelCache) .put(eq(model), eq(width), eq(height), any(GlideUrl.class)); urlLoader.buildLoadData(model, width, height, options); verify(modelCache).put(eq(model), eq(width), eq(height), any(GlideUrl.class)); } @Test public void testDoesNotInteractWithModelCacheIfNull() { TestLoader urlLoader = new TestLoader(wrapped, null); urlLoader.resultUrl = "fakeUrl"; int width = 456; int height = 789; when(wrapped.buildLoadData(any(GlideUrl.class), eq(width), eq(height), eq(options))) .thenReturn(new ModelLoader.LoadData<>(mock(Key.class), fetcher)); assertEquals( fetcher, Preconditions.checkNotNull(urlLoader.buildLoadData(new Object(), width, height, options)) .fetcher); } private static final class TestLoader extends BaseGlideUrlLoader { String resultUrl; TestLoader( ModelLoader concreteLoader, ModelCache modelCache) { super(concreteLoader, modelCache); } @Override protected String getUrl(Object model, int width, int height, Options options) { return resultUrl; } @Override public boolean handles(@NonNull Object model) { return true; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/model/stream/HttpGlideUrlLoaderTest.java ================================================ package com.bumptech.glide.load.model.stream; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.HttpUrlFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.util.Preconditions; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class HttpGlideUrlLoaderTest { private HttpGlideUrlLoader loader; private GlideUrl model; @SuppressWarnings("unchecked") @Before public void setUp() { loader = new HttpGlideUrlLoader(); model = mock(GlideUrl.class); } @Test public void testReturnsValidFetcher() { DataFetcher result = Preconditions.checkNotNull(loader.buildLoadData(model, 100, 100, new Options())).fetcher; assertThat(result).isInstanceOf(HttpUrlFetcher.class); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/SimpleResourceTest.java ================================================ package com.bumptech.glide.load.resource; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class SimpleResourceTest { private Anything object; private SimpleResource resource; @Before public void setUp() { object = new Anything(); resource = new SimpleResource<>(object); } @Test public void testReturnsGivenObject() { assertEquals(object, resource.get()); } @Test public void testReturnsGivenObjectMultipleTimes() { assertEquals(object, resource.get()); assertEquals(object, resource.get()); assertEquals(object, resource.get()); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullData() { new SimpleResource<>(null); } private static class Anything {} } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/UnitTransformationTest.java ================================================ package com.bumptech.glide.load.resource; import static com.bumptech.glide.tests.Util.mockResource; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import android.app.Application; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class UnitTransformationTest { @Rule public final KeyTester keyTester = new KeyTester(); private Application app; @Before public void setUp() { app = ApplicationProvider.getApplicationContext(); } @Test public void testReturnsGivenResource() { Resource resource = mockResource(); UnitTransformation transformation = UnitTransformation.get(); assertEquals(resource, transformation.transform(app, resource, 10, 10)); } @Test public void testEqualsHashCodeDigest() throws NoSuchAlgorithmException { @SuppressWarnings("unchecked") Transformation other = mock(Transformation.class); doAnswer(new Util.WriteDigest("other")) .when(other) .updateDiskCacheKey(any(MessageDigest.class)); keyTester .addEquivalenceGroup(UnitTransformation.get(), UnitTransformation.get()) .addEquivalenceGroup(other) .addEmptyDigestRegressionTest(UnitTransformation.get()) .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/BitmapDrawableResourceTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapDrawableResourceTest { private BitmapDrawableResourceHarness harness; @Before public void setUp() { harness = new BitmapDrawableResourceHarness(); } @Test public void testReturnsGivenBitmapFromGet() { assertEquals(harness.bitmap, harness.create().get().getBitmap()); } @Test public void testReturnsDifferentDrawableEachTime() { BitmapDrawableResource resource = harness.create(); BitmapDrawable first = resource.get(); BitmapDrawable second = resource.get(); assertNotSame(first, second); } @Test public void testReturnsSizeFromGivenBitmap() { assertEquals( harness.bitmap.getHeight() * harness.bitmap.getRowBytes(), harness.create().getSize()); } @Test public void testBitmapIsReturnedToPoolOnRecycle() { harness.create().recycle(); verify(harness.bitmapPool).put(eq(harness.bitmap)); } private static class BitmapDrawableResourceHarness { final BitmapPool bitmapPool = mock(BitmapPool.class); final Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); BitmapDrawableResource create() { return new BitmapDrawableResource( new BitmapDrawable(ApplicationProvider.getApplicationContext().getResources(), bitmap), bitmapPool); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/BitmapDrawableTransformationTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.anyContext; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) @SuppressWarnings("deprecation") public class BitmapDrawableTransformationTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private BitmapPool bitmapPool; @Mock private Transformation wrapped; @Mock private Resource drawableResourceToTransform; private BitmapDrawableTransformation transformation; private Bitmap bitmapToTransform; private Application context; @Before public void setUp() { MockitoAnnotations.initMocks(this); bitmapToTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); BitmapDrawable drawableToTransform = new BitmapDrawable(bitmapToTransform); context = ApplicationProvider.getApplicationContext(); Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool)); when(drawableResourceToTransform.get()).thenReturn(drawableToTransform); transformation = new BitmapDrawableTransformation(wrapped); } @After public void tearDown() { Glide.tearDown(); } @Test public void testReturnsOriginalResourceIfTransformationDoesNotTransform() { int outWidth = 123; int outHeight = 456; when(wrapped.transform(anyContext(), Util.anyResource(), eq(outWidth), eq(outHeight))) .thenAnswer( new Answer>() { @SuppressWarnings("unchecked") @Override public Resource answer(InvocationOnMock invocation) throws Throwable { return (Resource) invocation.getArguments()[1]; } }); Resource transformed = transformation.transform(context, drawableResourceToTransform, outWidth, outHeight); assertThat(transformed).isEqualTo(drawableResourceToTransform); } @Test public void testReturnsNewResourceIfTransformationDoesTransform() { int outWidth = 999; int outHeight = 555; Bitmap transformedBitmap = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.RGB_565); Resource transformedBitmapResource = Util.mockResource(); when(transformedBitmapResource.get()).thenReturn(transformedBitmap); when(wrapped.transform(anyContext(), Util.anyResource(), eq(outWidth), eq(outHeight))) .thenReturn(transformedBitmapResource); Resource transformed = transformation.transform(context, drawableResourceToTransform, outWidth, outHeight); assertThat(transformed.get().getBitmap()).isEqualTo(transformedBitmap); } @Test public void testProvidesBitmapFromGivenResourceToWrappedTransformation() { int outWidth = 332; int outHeight = 111; Resource transformed = Util.mockResource(); when(transformed.get()) .thenReturn(Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888)); when(wrapped.transform(anyContext(), Util.anyResource(), anyInt(), anyInt())) .thenReturn(transformed); transformation.transform(context, drawableResourceToTransform, outWidth, outHeight); ArgumentCaptor> captor = Util.cast(ArgumentCaptor.forClass(Resource.class)); verify(wrapped).transform(anyContext(), captor.capture(), eq(outWidth), eq(outHeight)); assertThat(captor.getValue().get()).isEqualTo(bitmapToTransform); } @Test public void testEquals() throws NoSuchAlgorithmException { doAnswer(new Util.WriteDigest("wrapped")) .when(wrapped) .updateDiskCacheKey(any(MessageDigest.class)); @SuppressWarnings("unchecked") Transformation other = mock(Transformation.class); doAnswer(new Util.WriteDigest("other")) .when(other) .updateDiskCacheKey(any(MessageDigest.class)); keyTester .addEquivalenceGroup(transformation, new BitmapDrawableTransformation(wrapped)) .addEquivalenceGroup(new BitmapDrawableTransformation(other)) .addEquivalenceGroup(wrapped) .addRegressionTest( transformation, "adbf45b08ad6468aa147e5b2a23758ef56ab631a2b70ad52501ca358441a34f3") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/BitmapEncoderTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.mockResource; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.EncodeStrategy; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.util.ByteBufferUtil; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapEncoderTest { private EncoderHarness harness; @Before public void setUp() { harness = new EncoderHarness(); } @After public void tearDown() { harness.tearDown(); } @Test public void testBitmapIsEncoded() throws IOException { harness.bitmap.setHasAlpha(false); assertThat(harness.encode()).isEqualTo(harness.expectedData(CompressFormat.JPEG, 90)); } @Test public void testBitmapIsEncodedWithGivenQuality() throws IOException { int quality = 7; harness.setQuality(quality); harness.bitmap.setHasAlpha(false); assertThat(harness.encode()).isEqualTo(harness.expectedData(CompressFormat.JPEG, quality)); } @Test public void testEncoderObeysNonNullCompressFormat() throws IOException { Bitmap.CompressFormat format = Bitmap.CompressFormat.WEBP; harness.setFormat(format); assertThat(harness.encode()).isEqualTo(harness.expectedData(CompressFormat.WEBP, 90)); } @Test public void testEncoderEncodesJpegWithNullFormatAndBitmapWithoutAlpha() throws IOException { harness.setFormat(null); harness.bitmap.setHasAlpha(false); assertThat(harness.encode()).isEqualTo(harness.expectedData(CompressFormat.JPEG, 90)); } @Test public void testEncoderEncodesPngWithNullFormatAndBitmapWithAlpha() throws IOException { harness.setFormat(null); harness.bitmap.setHasAlpha(true); assertThat(harness.encode()).isEqualTo(harness.expectedData(CompressFormat.PNG, 90)); } @Test public void testReturnsTrueFromWrite() { BitmapEncoder encoder = new BitmapEncoder(harness.arrayPool); assertTrue(encoder.encode(harness.resource, harness.file, harness.options)); } @Test public void testEncodeStrategy_alwaysReturnsTransformed() { BitmapEncoder encoder = new BitmapEncoder(harness.arrayPool); assertEquals(EncodeStrategy.TRANSFORMED, encoder.getEncodeStrategy(harness.options)); } private static class EncoderHarness { final Resource resource = mockResource(); final Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); final Options options = new Options(); final File file = new File(ApplicationProvider.getApplicationContext().getCacheDir(), "test"); final ArrayPool arrayPool = new LruArrayPool(); EncoderHarness() { when(resource.get()).thenReturn(bitmap); } void setQuality(int quality) { options.set(BitmapEncoder.COMPRESSION_QUALITY, quality); } void setFormat(Bitmap.CompressFormat format) { options.set(BitmapEncoder.COMPRESSION_FORMAT, format); } byte[] encode() throws IOException { BitmapEncoder encoder = new BitmapEncoder(arrayPool); encoder.encode(resource, file, options); return ByteBufferUtil.toBytes(ByteBufferUtil.fromFile(file)); } byte[] expectedData(CompressFormat expectedFormat, int expectedQuality) { ByteArrayOutputStream os = new ByteArrayOutputStream(); bitmap.compress(expectedFormat, expectedQuality, os); return os.toByteArray(); } void tearDown() { // GC before delete() to release files on Windows (https://stackoverflow.com/a/4213208/253468) System.gc(); if (file.exists() && !file.delete()) { throw new IllegalStateException("Failed to delete: " + file); } } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/BitmapResourceTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.graphics.Bitmap; import android.os.Build; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.tests.Util; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; // TODO: add a test for bitmap size using getAllocationByteSize when robolectric supports kitkat. @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapResourceTest { private int currentBuildVersion; private BitmapResourceHarness harness; @Before public void setUp() { currentBuildVersion = Build.VERSION.SDK_INT; harness = new BitmapResourceHarness(); } @After public void tearDown() { Util.setSdkVersionInt(currentBuildVersion); } @Test public void testCanGetBitmap() { assertEquals(harness.bitmap, harness.resource.get()); } @Test public void testSizeIsBasedOnDimensPreKitKat() { Util.setSdkVersionInt(18); assertEquals( harness.bitmap.getWidth() * harness.bitmap.getHeight() * 4, harness.resource.getSize()); } @Test public void testPutsBitmapInPoolOnRecycle() { harness.resource.recycle(); verify(harness.bitmapPool).put(eq(harness.bitmap)); } @Test(expected = NullPointerException.class) public void testThrowsIfBitmapIsNull() { new BitmapResource(null, mock(BitmapPool.class)); } @Test(expected = NullPointerException.class) public void testThrowsIfBitmapPoolIsNull() { new BitmapResource(Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565), null); } @Test(expected = NullPointerException.class) public void testThrowsIfBitmapAndBitmapPoolAreNull() { new BitmapResource(null, null); } private static class BitmapResourceHarness { final Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); final BitmapPool bitmapPool = mock(BitmapPool.class); final BitmapResource resource = new BitmapResource(bitmap, bitmapPool); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/BitmapTransformationTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.when; import android.app.Application; import android.graphics.Bitmap; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapTransformationTest { @Mock private BitmapPool bitmapPool; private Application context; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool)); } @After public void tearDown() { Glide.tearDown(); } @Test public void testReturnsGivenResourceWhenBitmapNotTransformed() { BitmapTransformation transformation = new BitmapTransformation() { @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {} @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return toTransform; } }; Resource resource = mockResource(100, 100); assertEquals(resource, transformation.transform(context, resource, 1, 1)); } @Test public void testReturnsNewResourceWhenBitmapTransformed() { final Bitmap transformed = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); BitmapTransformation transformation = new BitmapTransformation() { @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {} @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap bitmap, int outWidth, int outHeight) { return transformed; } }; Resource resource = mockResource(1, 2); assertNotSame(resource, transformation.transform(context, resource, 100, 100)); } @Test public void testPassesGivenArgumentsToTransform() { final int expectedWidth = 13; final int expectedHeight = 148; final Resource resource = mockResource(223, 4123); BitmapTransformation transformation = new BitmapTransformation() { @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {} @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { assertEquals(bitmapPool, pool); assertEquals(resource.get(), toTransform); assertEquals(expectedWidth, outWidth); assertEquals(expectedHeight, outHeight); return resource.get(); } }; transformation.transform(context, resource, expectedWidth, expectedHeight); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfGivenInvalidWidth() { BitmapTransformation transformation = new BitmapTransformation() { @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {} @Override protected Bitmap transform( @NonNull BitmapPool bitmapPool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return null; } }; transformation.transform(context, mockResource(1, 1), -1, 100); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfGivenInvalidHeight() { BitmapTransformation transformation = new BitmapTransformation() { @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {} @Override protected Bitmap transform( @NonNull BitmapPool bitmapPool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return null; } }; transformation.transform(context, mockResource(1, 1), 100, -1); } @Test public void testReturnsNullIfTransformReturnsNull() { BitmapTransformation transform = new BitmapTransformation() { @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {} @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { return null; } }; Resource resource = mockResource(100, 100); assertNull(transform.transform(context, resource, 100, 100)); } @Test public void testCallsTransformWithGivenBitmapWidthIfWidthIsSizeOriginal() { SizeTrackingTransform transform = new SizeTrackingTransform(); int expectedWidth = 200; Resource resource = mockResource(expectedWidth, 300); transform.transform(context, resource, Target.SIZE_ORIGINAL, 500); assertEquals(expectedWidth, transform.givenWidth); } @Test public void testCallsTransformWithGivenBitmapHeightIfHeightIsSizeOriginal() { SizeTrackingTransform transform = new SizeTrackingTransform(); int expectedHeight = 500; Resource resource = mockResource(123, expectedHeight); transform.transform(context, resource, 444, expectedHeight); assertEquals(expectedHeight, transform.givenHeight); } private Resource mockResource(int width, int height) { Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Resource resource = Util.mockResource(); when(resource.get()).thenReturn(bitmap); return resource; } private static final class SizeTrackingTransform extends BitmapTransformation { int givenWidth; int givenHeight; @Override protected Bitmap transform( @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { givenWidth = outWidth; givenHeight = outHeight; return null; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {} } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/CenterCropTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = 28) public class CenterCropTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Resource resource; @Mock private BitmapPool pool; @Mock private Transformation transformation; private CenterCrop centerCrop; private int bitmapWidth; private int bitmapHeight; private Bitmap bitmap; private Application context; @Before public void setUp() { MockitoAnnotations.initMocks(this); bitmapWidth = 100; bitmapHeight = 100; bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); when(resource.get()).thenReturn(bitmap); when(pool.get(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenAnswer(new Util.CreateBitmap()); context = ApplicationProvider.getApplicationContext(); Glide.init(context, new GlideBuilder().setBitmapPool(pool)); centerCrop = new CenterCrop(); } @After public void tearDown() { Glide.tearDown(); } @Test public void testDoesNotPutNullBitmapAcquiredFromPool() { reset(pool); when(pool.get(anyInt(), anyInt(), any(Bitmap.Config.class))).thenReturn(null); centerCrop.transform(context, resource, 100, 100); verify(pool, never()).put(any(Bitmap.class)); } @Test public void testReturnsGivenResourceIfMatchesSizeExactly() { Resource result = centerCrop.transform(context, resource, bitmapWidth, bitmapHeight); assertEquals(resource, result); } @Test public void testDoesNotRecycleGivenResourceIfMatchesSizeExactly() { centerCrop.transform(context, resource, bitmapWidth, bitmapHeight); verify(resource, never()).recycle(); } @Test public void testDoesNotRecycleGivenResource() { centerCrop.transform(context, resource, 50, 50); verify(resource, never()).recycle(); } @Test @Config(sdk = 19) public void testAsksBitmapPoolForArgb8888IfInConfigIsNull() { bitmap.setConfig(null); centerCrop.transform(context, resource, 10, 10); verify(pool).get(anyInt(), anyInt(), eq(Bitmap.Config.ARGB_8888)); verify(pool, never()).get(anyInt(), anyInt(), (Bitmap.Config) isNull()); } @Test public void testReturnsBitmapWithExactlyGivenDimensionsIfBitmapIsLargerThanTarget() { int expectedWidth = 75; int expectedHeight = 74; for (int[] dimens : new int[][] {new int[] {800, 200}, new int[] {450, 100}, new int[] {78, 78}}) { Bitmap toTransform = Bitmap.createBitmap(dimens[0], dimens[1], Bitmap.Config.ARGB_4444); when(resource.get()).thenReturn(toTransform); Resource result = centerCrop.transform(context, resource, expectedWidth, expectedHeight); Bitmap transformed = result.get(); assertEquals(expectedWidth, transformed.getWidth()); assertEquals(expectedHeight, transformed.getHeight()); } } @Test public void testReturnsBitmapWithExactlyGivenDimensionsIfBitmapIsSmallerThanTarget() { int expectedWidth = 100; int expectedHeight = 100; for (int[] dimens : new int[][] {new int[] {50, 90}, new int[] {150, 2}, new int[] {78, 78}}) { Bitmap toTransform = Bitmap.createBitmap(dimens[0], dimens[1], Bitmap.Config.ARGB_4444); when(resource.get()).thenReturn(toTransform); Resource result = centerCrop.transform(context, resource, expectedWidth, expectedHeight); Bitmap transformed = result.get(); assertEquals(expectedWidth, transformed.getWidth()); assertEquals(expectedHeight, transformed.getHeight()); } } @Test public void testEquals() throws NoSuchAlgorithmException { doAnswer(new Util.WriteDigest("other")) .when(transformation) .updateDiskCacheKey(any(MessageDigest.class)); keyTester .addEquivalenceGroup(new CenterCrop(), new CenterCrop()) .addEquivalenceGroup(transformation) .addRegressionTest( new CenterCrop(), "68bd5819c42b37efbe7124bb851443a6388ee3e2e9034213da6eaa15381d3457") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/CenterInsideTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class CenterInsideTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Resource resource; @Mock private Transformation transformation; private CenterInside centerInside; private int bitmapWidth; private int bitmapHeight; private Application context; @Before public void setUp() { MockitoAnnotations.initMocks(this); bitmapWidth = 100; bitmapHeight = 100; Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); when(resource.get()).thenReturn(bitmap); context = ApplicationProvider.getApplicationContext(); BitmapPool pool = new BitmapPoolAdapter(); Glide.init(context, new GlideBuilder().setBitmapPool(pool)); centerInside = new CenterInside(); } @After public void tearDown() { Glide.tearDown(); } @Test public void testReturnsGivenResourceIfMatchesSizeExactly() { Resource result = centerInside.transform(context, resource, bitmapWidth, bitmapHeight); assertEquals(resource, result); } @Test public void testReturnsGivenResourceIfSmallerThanTarget() { Resource result = centerInside.transform(context, resource, 150, 150); assertEquals(resource, result); } @Test public void testReturnsNewResourceIfLargerThanTarget() { Resource result = centerInside.transform(context, resource, 50, 50); assertNotEquals(resource, result); } @Test public void testDoesNotRecycleGivenResourceIfMatchesSizeExactly() { centerInside.transform(context, resource, bitmapWidth, bitmapHeight); verify(resource, never()).recycle(); } @Test public void testDoesNotRecycleGivenResource() { centerInside.transform(context, resource, 50, 50); verify(resource, never()).recycle(); } @Test public void testEquals() throws NoSuchAlgorithmException { doAnswer(new Util.WriteDigest("other")) .when(transformation) .updateDiskCacheKey(any(MessageDigest.class)); keyTester .addEquivalenceGroup(new CenterInside(), new CenterInside(), centerInside) .addEquivalenceGroup(transformation) .addRegressionTest( new CenterInside(), "acf83850a2e8e9e809c8bfb999e2aede9e932cb897a15367fac9856b96f3ba33") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/CircleCropTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class CircleCropTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private BitmapPool bitmapPool; private CircleCrop circleCrop; @Before public void setup() { MockitoAnnotations.initMocks(this); when(bitmapPool.get(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenAnswer(new Util.CreateBitmap()); Context context = ApplicationProvider.getApplicationContext(); Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool)); circleCrop = new CircleCrop(); } @After public void tearDown() { Glide.tearDown(); } @Test public void testTransform_withSquare() { Bitmap redSquare = createSolidRedBitmap(50, 50); Bitmap result = circleCrop.transform(bitmapPool, redSquare, 50, 50); Bitmap expected = createBitmapWithRedCircle(50, 50); assertSamePixels(expected, result); } @Test public void testTransform_reusesBitmap() { Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); when(bitmapPool.get(50, 50, Bitmap.Config.ARGB_8888)).thenReturn(toReuse); Bitmap redSquare = createSolidRedBitmap(50, 50); Bitmap result = circleCrop.transform(bitmapPool, redSquare, 50, 50); assertEquals(toReuse, result); } @Test public void testTransform_withWideRectangle() { Bitmap redWideRectangle = createSolidRedBitmap(100, 50); Bitmap result = circleCrop.transform(bitmapPool, redWideRectangle, 80, 50); Bitmap expected = createBitmapWithRedCircle(80, 50); assertSamePixels(expected, result); } @Test public void testTransform_withNarrowRectangle() { Bitmap redNarrowRectangle = createSolidRedBitmap(20, 50); Bitmap result = circleCrop.transform(bitmapPool, redNarrowRectangle, 40, 80); Bitmap expected = createBitmapWithRedCircle(40, 80); assertSamePixels(expected, result); } @Test public void testEquals() { keyTester .addEquivalenceGroup(circleCrop, new CircleCrop()) .addEquivalenceGroup(mock(Transformation.class)) .addRegressionTest( new CircleCrop(), "1442365bcc658f89310e39844ef4be58f4b16e52c283254e5a458020f56acb90") .test(); } private void assertSamePixels(Bitmap expected, Bitmap actual) { assertEquals(expected.getWidth(), actual.getWidth()); assertEquals(expected.getHeight(), actual.getHeight()); assertEquals(expected.getConfig(), actual.getConfig()); for (int y = 0; y < expected.getHeight(); y++) { for (int x = 0; x < expected.getWidth(); x++) { assertEquals(expected.getPixel(x, y), actual.getPixel(x, y)); } } } private Bitmap createBitmapWithRedCircle(int width, int height) { int minEdge = Math.min(width, height); float radius = minEdge / 2f; Bitmap result = Bitmap.createBitmap(minEdge, minEdge, Bitmap.Config.ARGB_8888); result.setHasAlpha(true); Canvas canvas = new Canvas(result); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); canvas.drawCircle(radius, radius, radius, paint); return result; } private Bitmap createSolidRedBitmap(int width, int height) { Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(result); Paint paint = new Paint(); paint.setColor(Color.RED); Rect rect = new Rect(0, 0, width, height); canvas.drawRect(rect, paint); return result; } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParserTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import androidx.annotation.NonNull; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.testutil.TestResourceUtil; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class DefaultImageHeaderParserTest { private static final byte[] PNG_HEADER_WITH_IHDR_CHUNK = new byte[] { (byte) 0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x1, (byte) 0x90, 0x0, 0x0, 0x1, 0x2c, 0x8, 0x6 }; private ArrayPool byteArrayPool; @Before public void setUp() { byteArrayPool = new LruArrayPool(); } @Test public void testCanParsePngType() throws IOException { // PNG magic number from: http://en.wikipedia.org/wiki/Portable_Network_Graphics. byte[] data = new byte[] {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.PNG, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.PNG, parser.getType(byteBuffer)); } }); } @Test public void testCanParsePngWithAlpha() throws IOException { for (int i = 3; i <= 6; i++) { byte[] pngHeaderWithIhdrChunk = generatePngHeaderWithIhdr(i); runTest( pngHeaderWithIhdrChunk, new ParserTestCase() { @Override public void run( DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.PNG_A, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.PNG_A, parser.getType(byteBuffer)); } }); } } @Test public void testCanParsePngWithoutAlpha() throws IOException { for (int i = 0; i < 3; i++) { byte[] pngHeaderWithIhdrChunk = generatePngHeaderWithIhdr(i); runTest( pngHeaderWithIhdrChunk, new ParserTestCase() { @Override public void run( DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.PNG, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.PNG, parser.getType(byteBuffer)); } }); } } @Test public void testCanParseJpegType() throws IOException { byte[] data = new byte[] {(byte) 0xFF, (byte) 0xD8}; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.JPEG, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.JPEG, parser.getType(byteBuffer)); } }); } @Test public void testCanParseGifType() throws IOException { byte[] data = new byte[] {'G', 'I', 'F'}; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.GIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.GIF, parser.getType(byteBuffer)); } }); } @Test public void testCanParseLosslessWebpWithAlpha() throws IOException { byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x3c, 0x50, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x4c, // Lossless 0x30, 0x50, 0x00, 0x00, 0x2f, // Flags (byte) 0xef, (byte) 0x80, 0x15, 0x10, (byte) 0x8d, 0x30, 0x68, 0x1b, (byte) 0xc9, (byte) 0x91, (byte) 0xb2 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP_A, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP_A, parser.getType(byteBuffer)); } }); } @Test public void testCanParseLosslessWebpWithoutAlpha() throws IOException { byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x3c, 0x50, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x4c, // Lossless 0x30, 0x50, 0x00, 0x00, 0x00, // Flags (byte) 0xef, (byte) 0x80, 0x15, 0x10, (byte) 0x8d, 0x30, 0x68, 0x1b, (byte) 0xc9, (byte) 0x91, (byte) 0xb2 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP, parser.getType(byteBuffer)); } }); } @Test public void testCanParseExtendedWebpWithAlpha() throws IOException { byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x3c, 0x50, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, // Extended 0x30, 0x50, 0x00, 0x00, 0x10, // flags (byte) 0xef, (byte) 0x80, 0x15, 0x10, (byte) 0x8d, 0x30, 0x68, 0x1b, (byte) 0xc9, (byte) 0x91, (byte) 0xb2 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP_A, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP_A, parser.getType(byteBuffer)); } }); } @Test public void testCanParseExtendedWebpWithoutAlpha() throws IOException { byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x3c, 0x50, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, // Extended 0x30, 0x50, 0x00, 0x00, 0x00, // flags (byte) 0xef, (byte) 0x80, 0x15, 0x10, (byte) 0x8d, 0x30, 0x68, 0x1b, (byte) 0xc9, (byte) 0x91, (byte) 0xb2 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.WEBP, parser.getType(byteBuffer)); } }); } @Test public void testCanParseExtendedWebpWithoutAlphaAndWithAnimation() throws IOException { byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x3c, 0x50, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, // Extended 0x30, 0x50, 0x00, 0x00, 0x02, // Flags (byte) 0xef, (byte) 0x80, 0x15, 0x10, (byte) 0x8d, 0x30, 0x68, 0x1b, (byte) 0xc9, (byte) 0x91, (byte) 0xb2 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_WEBP, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_WEBP, parser.getType(byteBuffer)); } }); } @Test public void testCanParseExtendedWebpWithAlphaAndAnimation() throws IOException { byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x3c, 0x50, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, // Extended 0x30, 0x50, 0x00, 0x00, (byte) 0x12, // Flags (byte) 0xef, (byte) 0x80, 0x15, 0x10, (byte) 0x8d, 0x30, 0x68, 0x1b, (byte) 0xc9, (byte) 0x91, (byte) 0xb2 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_WEBP, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_WEBP, parser.getType(byteBuffer)); } }); } @Test public void testCanParseRealAnimatedWebpFile() throws IOException { byte[] data = ByteStreams.toByteArray(TestResourceUtil.openResource(getClass(), "animated_webp.webp")); runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertThat(parser.getType(is)).isEqualTo(ImageType.ANIMATED_WEBP); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertThat(parser.getType(byteBuffer)).isEqualTo(ImageType.ANIMATED_WEBP); } }); } @Test public void testCanParseAvifMajorBrand() throws IOException { byte[] data = new byte[] { // Box Size. 0x00, 0x00, 0x00, 0x1C, // ftyp. 0x66, 0x74, 0x79, 0x70, // avif (major brand). 0x61, 0x76, 0x69, 0x66, // minor version. 0x00, 0x00, 0x00, 0x00, // other minor brands (mif1, miaf, MA1B). 0x6d, 0x69, 0x66, 0x31, 0x6d, 0x69, 0x61, 0x66, 0x4d, 0x41, 0x31, 0x42 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.AVIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.AVIF, parser.getType(byteBuffer)); } }); // Change the major brand from 'avif' to 'avis'. Now, the expected output is ANIMATED_AVIF. data[11] = 0x73; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer)); } }); } @Test public void testCanParseAvifMinorBrand() throws IOException { byte[] data = new byte[] { // Box Size. 0x00, 0x00, 0x00, 0x1C, // ftyp. 0x66, 0x74, 0x79, 0x70, // mif1 (major brand). 0x6d, 0x69, 0x66, 0x31, // minor version. 0x00, 0x00, 0x00, 0x00, // other minor brands (miaf, avif, MA1B). 0x6d, 0x69, 0x61, 0x66, 0x61, 0x76, 0x69, 0x66, 0x4d, 0x41, 0x31, 0x42 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.AVIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.AVIF, parser.getType(byteBuffer)); } }); // Change the last minor brand from 'MA1B' to 'avis'. Now, the expected output is ANIMATED_AVIF. data[24] = 0x61; data[25] = 0x76; data[26] = 0x69; data[27] = 0x73; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer)); } }); } @Test public void testCanParseAvifAndAvisBrandsAsAnimatedAvif() throws IOException { byte[] data = new byte[] { // Box Size. 0x00, 0x00, 0x00, 0x1C, // ftyp. 0x66, 0x74, 0x79, 0x70, // avis (major brand). 0x61, 0x76, 0x69, 0x73, // minor version. 0x00, 0x00, 0x00, 0x00, // other minor brands (miaf, avif, MA1B). 0x6d, 0x69, 0x61, 0x66, 0x61, 0x76, 0x69, 0x66, 0x4d, 0x41, 0x31, 0x42 }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer)); } }); // Change the major brand from 'avis' to 'avif'. data[11] = 0x66; // Change the minor brand from 'avif' to 'avis'. data[23] = 0x73; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer)); } }); } @Test public void testCannotParseAvifMoreThanFiveMinorBrands() throws IOException { byte[] data = new byte[] { // Box Size. 0x00, 0x00, 0x00, 0x28, // ftyp. 0x66, 0x74, 0x79, 0x70, // mif1 (major brand). 0x6d, 0x69, 0x66, 0x31, // minor version. 0x00, 0x00, 0x00, 0x00, // more than five minor brands with the sixth one being avif (mif1, miaf, MA1B, mif1, // miab, avif). 0x6d, 0x69, 0x66, 0x31, 0x6d, 0x69, 0x61, 0x66, 0x4d, 0x41, 0x31, 0x42, 0x6d, 0x69, 0x66, 0x31, 0x6d, 0x69, 0x61, 0x66, 0x61, 0x76, 0x69, 0x66, }; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertNotEquals(ImageType.AVIF, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertNotEquals(ImageType.AVIF, parser.getType(byteBuffer)); } }); } @Test public void testCanParseRealAnimatedAvifFile() throws IOException { byte[] data = ByteStreams.toByteArray(TestResourceUtil.openResource(getClass(), "animated_avif.avif")); runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertThat(parser.getType(is)).isEqualTo(ImageType.ANIMATED_AVIF); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertThat(parser.getType(byteBuffer)).isEqualTo(ImageType.ANIMATED_AVIF); } }); } @Test public void testReturnsUnknownTypeForUnknownImageHeaders() throws IOException { byte[] data = new byte[] {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.UNKNOWN, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.UNKNOWN, parser.getType(byteBuffer)); } }); } // Test for #286. @Test public void testHandlesParsingOrientationWithMinimalExifSegment() throws IOException { byte[] data = ByteStreams.toByteArray(TestResourceUtil.openResource(getClass(), "short_exif_sample.jpg")); runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(-1, parser.getOrientation(is, byteArrayPool)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(-1, parser.getOrientation(byteBuffer, byteArrayPool)); } }); } @Test public void testReturnsUnknownForEmptyData() throws IOException { runTest( new byte[0], new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.UNKNOWN, parser.getType(is)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(ImageType.UNKNOWN, parser.getType(byteBuffer)); } }); } // Test for #387. @Test public void testHandlesPartialReads() throws IOException { InputStream is = TestResourceUtil.openResource(getClass(), "issue387_rotated_jpeg.jpg"); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertThat(parser.getOrientation(new PartialReadInputStream(is), byteArrayPool)).isEqualTo(6); } // Test for #387. @Test public void testHandlesPartialSkips() throws IOException { InputStream is = TestResourceUtil.openResource(getClass(), "issue387_rotated_jpeg.jpg"); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertThat(parser.getOrientation(new PartialSkipInputStream(is), byteArrayPool)).isEqualTo(6); } @Test public void testHandlesSometimesZeroSkips() throws IOException { InputStream is = new ByteArrayInputStream( new byte[] {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertEquals(ImageType.PNG, parser.getType(new SometimesZeroSkipInputStream(is))); } @Test public void getOrientation_withExifSegmentLessThanLength_returnsUnknown() throws IOException { ByteBuffer jpegHeaderBytes = getExifMagicNumber(); byte[] data = new byte[] { jpegHeaderBytes.get(0), jpegHeaderBytes.get(1), (byte) DefaultImageHeaderParser.SEGMENT_START_ID, (byte) DefaultImageHeaderParser.EXIF_SEGMENT_TYPE, // SEGMENT_LENGTH (byte) 0xFF, (byte) 0xFF, }; ByteBuffer byteBuffer = ByteBuffer.wrap(data); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertEquals( ImageHeaderParser.UNKNOWN_ORIENTATION, parser.getOrientation(byteBuffer, byteArrayPool)); } @Test public void getOrientation_withNonExifSegmentLessThanLength_returnsUnknown() throws IOException { ByteBuffer jpegHeaderBytes = getExifMagicNumber(); byte[] data = new byte[] { jpegHeaderBytes.get(0), jpegHeaderBytes.get(1), (byte) DefaultImageHeaderParser.SEGMENT_START_ID, // SEGMENT_TYPE (NOT EXIF_SEGMENT_TYPE) (byte) 0xE5, // SEGMENT_LENGTH (byte) 0xFF, (byte) 0xFF, }; ByteBuffer byteBuffer = ByteBuffer.wrap(data); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertEquals( ImageHeaderParser.UNKNOWN_ORIENTATION, parser.getOrientation(byteBuffer, byteArrayPool)); } @Test public void getOrientation_withExifSegmentAndPreambleButLessThanLength_returnsUnknown() throws IOException { ByteBuffer jpegHeaderBytes = getExifMagicNumber(); ByteBuffer exifSegmentPreamble = ByteBuffer.wrap(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES); ByteBuffer data = ByteBuffer.allocate(2 + 1 + 1 + 2 + exifSegmentPreamble.capacity()); data.put(jpegHeaderBytes) .put((byte) DefaultImageHeaderParser.SEGMENT_START_ID) .put((byte) DefaultImageHeaderParser.EXIF_SEGMENT_TYPE) // SEGMENT_LENGTH, add two because length includes the segment length short, and one to go // beyond the preamble bytes length for the test. .putShort( (short) (DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length + 2 + 1)) .put(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES); data.position(0); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertEquals(ImageHeaderParser.UNKNOWN_ORIENTATION, parser.getOrientation(data, byteArrayPool)); } @Test public void getOrientation_withExifSegmentAndPreambleBetweenLengthAndExpected_returnsUnknown() throws IOException { ByteBuffer jpegHeaderBytes = getExifMagicNumber(); ByteBuffer exifSegmentPreamble = ByteBuffer.wrap(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES); ByteBuffer data = ByteBuffer.allocate(2 + 1 + 1 + 2 + exifSegmentPreamble.capacity() + 2 + 1); data.put(jpegHeaderBytes) .put((byte) DefaultImageHeaderParser.SEGMENT_START_ID) .put((byte) DefaultImageHeaderParser.EXIF_SEGMENT_TYPE) // SEGMENT_LENGTH, add two because length includes the segment length short, and one to go // beyond the preamble bytes length for the test. .putShort( (short) (DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length + 2 + 1)) .put(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES); data.position(0); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); assertEquals(ImageHeaderParser.UNKNOWN_ORIENTATION, parser.getOrientation(data, byteArrayPool)); } @Test public void hasJpegMpf_withGainmapFile_returnsTrue() throws IOException { byte[] data = ByteStreams.toByteArray( TestResourceUtil.openResource(getClass(), "small_gainmap_image.jpg")); runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(true, parser.hasJpegMpf(is, byteArrayPool)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(true, parser.hasJpegMpf(byteBuffer, byteArrayPool)); } }); } @Test public void hasJpegMpf_withNonGainmapFile_returnsFalse() throws IOException { byte[] data = ByteStreams.toByteArray(TestResourceUtil.openResource(getClass(), "short_exif_sample.jpg")); runTest( data, new ParserTestCase() { @Override public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException { assertEquals(false, parser.hasJpegMpf(is, byteArrayPool)); } @Override public void run( DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException { assertEquals(false, parser.hasJpegMpf(byteBuffer, byteArrayPool)); } }); } private static ByteBuffer getExifMagicNumber() { ByteBuffer jpegHeaderBytes = ByteBuffer.allocate(2); jpegHeaderBytes.putShort((short) DefaultImageHeaderParser.EXIF_MAGIC_NUMBER); jpegHeaderBytes.position(0); return jpegHeaderBytes; } private interface ParserTestCase { void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool) throws IOException; void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException; } private static void runTest(byte[] data, ParserTestCase test) throws IOException { InputStream is = new ByteArrayInputStream(data); DefaultImageHeaderParser parser = new DefaultImageHeaderParser(); test.run(parser, is, new LruArrayPool()); ByteBuffer buffer = ByteBuffer.wrap(data); parser = new DefaultImageHeaderParser(); test.run(parser, buffer, new LruArrayPool()); } private static byte[] generatePngHeaderWithIhdr(int bitDepth) { byte[] result = new byte[PNG_HEADER_WITH_IHDR_CHUNK.length]; System.arraycopy(PNG_HEADER_WITH_IHDR_CHUNK, 0, result, 0, PNG_HEADER_WITH_IHDR_CHUNK.length); result[result.length - 1] = (byte) bitDepth; return result; } private static class SometimesZeroSkipInputStream extends FilterInputStream { boolean returnZeroFlag = true; SometimesZeroSkipInputStream(InputStream in) { super(in); } @Override public long skip(long byteCount) throws IOException { final long result; if (returnZeroFlag) { result = 0; } else { result = super.skip(byteCount); } returnZeroFlag = !returnZeroFlag; return result; } } private static class PartialSkipInputStream extends FilterInputStream { PartialSkipInputStream(InputStream in) { super(in); } @Override public long skip(long byteCount) throws IOException { long toActuallySkip = byteCount / 2; if (byteCount == 1) { toActuallySkip = 1; } return super.skip(toActuallySkip); } } private static class PartialReadInputStream extends FilterInputStream { PartialReadInputStream(InputStream in) { super(in); } @Override public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) throws IOException { int toActuallyRead = byteCount / 2; if (byteCount == 1) { toActuallyRead = 1; } return super.read(buffer, byteOffset, toActuallyRead); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DownsampleStrategyTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.google.common.truth.Truth.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = Config.OLDEST_SDK) public class DownsampleStrategyTest { @Test public void testAtMost_withSourceSmallerInOneDimensions_returnsScaleFactorForLargestDimension() { assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(100, 200, 200, 200)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(200, 100, 200, 200)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(270, 480, 724, 440)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(400, 200, 200, 200)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(800, 200, 200, 200)).isEqualTo(1 / 4f); } @Test public void testAtMost_withSourceExactlyEqualToRequested_returnsScaleFactorOfOne() { assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(100, 100, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(1234, 452, 1234, 452)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(341, 122, 341, 122)).isEqualTo(1f); } @Test public void testAtMost_withSourceLessThanTwiceRequestedSize_returnsScaleFactorOfTwo() { assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(150, 150, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(101, 101, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(199, 199, 100, 100)).isEqualTo(1 / 2f); } @Test public void testAtMost_withSourceGreaterThanRequestedSize_returnsPowerOfTwoScaleFactor() { assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(200, 200, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(300, 300, 100, 100)).isEqualTo(1 / 4f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(400, 400, 100, 100)).isEqualTo(1 / 4f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(1000, 200, 100, 100)).isEqualTo(1 / 16f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(1000, 1000, 100, 100)).isEqualTo(1 / 16f); } @Test public void testAtMost_withSourceGreaterInOneDimension_returnsScaleFactorOfLargestDimension() { assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(101, 200, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(199, 200, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(400, 200, 100, 100)).isEqualTo(1 / 4f); assertThat(DownsampleStrategy.AT_MOST.getScaleFactor(1000, 400, 100, 100)).isEqualTo(1 / 16f); } @Test public void testAtLeast_withSourceSmallerInOneDimension_returnsScaleFactorOfOne() { assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(100, 200, 200, 200)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(200, 100, 200, 200)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(270, 480, 724, 440)).isEqualTo(1f); } @Test public void testAtLeast_withSourceExactlyEqualToRequested_returnsScaleFactorOfOne() { assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(100, 100, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(1234, 452, 1234, 452)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(341, 122, 341, 122)).isEqualTo(1f); } @Test public void testAtLeast_withSourceLessThanTwiceRequestedSize_returnsScaleFactorOfOne() { assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(150, 150, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(101, 101, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(199, 199, 100, 100)).isEqualTo(1f); } @Test public void testAtLeast_withSourceGreaterThanRequestedSize_returnsPowerOfTwoScaleFactor() { assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(200, 200, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(300, 300, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(400, 400, 100, 100)).isEqualTo(1 / 4f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(1000, 200, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(1000, 1000, 100, 100)).isEqualTo(1 / 8f); } @Test public void testAtLeast_withSourceGreaterInOneDimension_returnsScaleFactorOfSmallestDimension() { assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(101, 200, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(199, 200, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(400, 200, 100, 100)).isEqualTo(1 / 2f); assertThat(DownsampleStrategy.AT_LEAST.getScaleFactor(1000, 400, 100, 100)).isEqualTo(1 / 4f); } @Test public void testCenterInside_scalesImageToFitWithinRequestedBounds() { assertThat(DownsampleStrategy.FIT_CENTER.getScaleFactor(100, 200, 300, 300)) .isEqualTo(300 / 200f); assertThat(DownsampleStrategy.FIT_CENTER.getScaleFactor(270, 480, 724, 440)) .isEqualTo(440 / 480f); assertThat(DownsampleStrategy.FIT_CENTER.getScaleFactor(100, 100, 100, 100)).isEqualTo(1f); } @Test public void testCenterOutside_scalesImageToFitAroundRequestedBounds() { assertThat(DownsampleStrategy.CENTER_OUTSIDE.getScaleFactor(100, 200, 300, 300)) .isEqualTo(300 / 100f); assertThat(DownsampleStrategy.CENTER_OUTSIDE.getScaleFactor(270, 480, 724, 440)) .isEqualTo(724 / 270f); assertThat(DownsampleStrategy.CENTER_OUTSIDE.getScaleFactor(100, 100, 100, 100)).isEqualTo(1f); } @Test public void testNone_alwaysReturnsOne() { assertThat(DownsampleStrategy.NONE.getScaleFactor(100, 100, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.NONE.getScaleFactor(200, 200, 100, 100)).isEqualTo(1f); assertThat(DownsampleStrategy.NONE.getScaleFactor(100, 100, 200, 200)).isEqualTo(1f); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DrawableTransformationTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.load.resource.SimpleResource; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class DrawableTransformationTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Transformation bitmapTransformation; private BitmapPool bitmapPool; private DrawableTransformation transformation; private Context context; @Before public void setUp() { MockitoAnnotations.initMocks(this); transformation = new DrawableTransformation(bitmapTransformation, /* isRequired= */ true); context = ApplicationProvider.getApplicationContext(); bitmapPool = new BitmapPoolAdapter(); Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool)); } @After public void tearDown() { Glide.tearDown(); } @Test public void transform_withBitmapDrawable_andUnitBitmapTransformation_doesNotRecycle() { when(bitmapTransformation.transform( any(Context.class), anyBitmapResource(), anyInt(), anyInt())) .thenAnswer(new ReturnGivenResource()); Bitmap bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); @SuppressWarnings("unchecked") Resource input = (Resource) (Resource) new BitmapDrawableResource(drawable, bitmapPool); transformation.transform(context, input, /* outWidth= */ 100, /* outHeight= */ 200); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void transform_withBitmapDrawable_andFunctionalBitmapTransformation_doesNotRecycle() { when(bitmapTransformation.transform( any(Context.class), anyBitmapResource(), anyInt(), anyInt())) .thenAnswer( new Answer>() { @Override public Resource answer(InvocationOnMock invocationOnMock) throws Throwable { return BitmapResource.obtain( Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888), bitmapPool); } }); Bitmap bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); @SuppressWarnings("unchecked") Resource input = (Resource) (Resource) new BitmapDrawableResource(drawable, bitmapPool); transformation.transform(context, input, /* outWidth= */ 100, /* outHeight= */ 200); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void transform_withColorDrawable_andUnitBitmapTransformation_recycles() { bitmapPool = mock(BitmapPool.class); Glide.tearDown(); Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool)); when(bitmapTransformation.transform( any(Context.class), anyBitmapResource(), anyInt(), anyInt())) .thenAnswer(new ReturnGivenResource()); ColorDrawable colorDrawable = new ColorDrawable(Color.RED); final Resource input = new SimpleResource(colorDrawable); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { Bitmap bitmap = (Bitmap) invocationOnMock.getArguments()[0]; assertThat(bitmap.getWidth()).isEqualTo(100); assertThat(bitmap.getHeight()).isEqualTo(200); return null; } }) .when(bitmapPool) .put(any(Bitmap.class)); when(bitmapPool.get(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenAnswer( new Answer() { @Override public Bitmap answer(InvocationOnMock invocationOnMock) throws Throwable { int width = (Integer) invocationOnMock.getArguments()[0]; int height = (Integer) invocationOnMock.getArguments()[1]; Bitmap.Config config = (Bitmap.Config) invocationOnMock.getArguments()[2]; return Bitmap.createBitmap(width, height, config); } }); transformation.transform(context, input, /* outWidth= */ 100, /* outHeight= */ 200); verify(bitmapPool).put(isA(Bitmap.class)); } @Test public void testEquals() { BitmapTransformation otherBitmapTransformation = mock(BitmapTransformation.class); doAnswer(new Util.WriteDigest("bitmapTransformation")) .when(bitmapTransformation) .updateDiskCacheKey(any(MessageDigest.class)); doAnswer(new Util.WriteDigest("otherBitmapTransformation")) .when(otherBitmapTransformation) .updateDiskCacheKey(any(MessageDigest.class)); keyTester .addEquivalenceGroup( transformation, new DrawableTransformation(bitmapTransformation, /* isRequired= */ true), new DrawableTransformation(bitmapTransformation, /* isRequired= */ false)) .addEquivalenceGroup(bitmapTransformation) .addEquivalenceGroup(otherBitmapTransformation) .addEquivalenceGroup( new DrawableTransformation(otherBitmapTransformation, /* isRequired= */ true), new DrawableTransformation(otherBitmapTransformation, /* isRequired= */ false)) .addRegressionTest( new DrawableTransformation(bitmapTransformation, /* isRequired= */ true), "eddf60c557a6315a489b8a3a19b12439a90381256289fbe9a503afa726230bd9") .addRegressionTest( new DrawableTransformation(otherBitmapTransformation, /* isRequired= */ false), "40931536ed0ec97c39d4be10c44f5b69a86030ec575317f5a0f17e15a0ea9be8") .test(); } @SuppressWarnings("unchecked") private static Resource anyBitmapResource() { return any(Resource.class); } private static final class ReturnGivenResource implements Answer> { @Override public Resource answer(InvocationOnMock invocationOnMock) throws Throwable { @SuppressWarnings("unchecked") Resource input = (Resource) invocationOnMock.getArguments()[1]; return input; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/FitCenterTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class FitCenterTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Resource resource; @Mock private Transformation transformation; private FitCenter fitCenter; private int bitmapWidth; private int bitmapHeight; private Application context; @Before public void setUp() { MockitoAnnotations.initMocks(this); bitmapWidth = 100; bitmapHeight = 100; Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); when(resource.get()).thenReturn(bitmap); BitmapPool pool = new BitmapPoolAdapter(); context = ApplicationProvider.getApplicationContext(); Glide.init(context, new GlideBuilder().setBitmapPool(pool)); fitCenter = new FitCenter(); } @After public void tearDown() { Glide.tearDown(); } @Test public void testReturnsGivenResourceIfMatchesSizeExactly() { Resource result = fitCenter.transform(context, resource, bitmapWidth, bitmapHeight); assertEquals(resource, result); } @Test public void testDoesNotRecycleGivenResourceIfMatchesSizeExactly() { fitCenter.transform(context, resource, bitmapWidth, bitmapHeight); verify(resource, never()).recycle(); } @Test public void testDoesNotRecycleGivenResource() { fitCenter.transform(context, resource, 50, 50); verify(resource, never()).recycle(); } @Test public void testEquals() throws NoSuchAlgorithmException { doAnswer(new Util.WriteDigest("other")) .when(transformation) .updateDiskCacheKey(any(MessageDigest.class)); keyTester .addEquivalenceGroup(fitCenter, new FitCenter(), new FitCenter()) .addEquivalenceGroup(transformation) .addRegressionTest( new FitCenter(), "eda03bc6969032145110add4bfe399915897406f4ca3a1a7512d07750e60f90d") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/HardwareConfigStateTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowBuild; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class HardwareConfigStateTest { private static final int VALID_DIMENSION = 100; @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withAllowedState_setsInPreferredConfigAndMutable_returnsTrue() { HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertThat(result).isTrue(); assertThat(options.inPreferredConfig).isEqualTo(Bitmap.Config.HARDWARE); } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withAllowedState_afterReblock_returnsFalseAndDoesNotSetValues() { HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); state.blockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertThat(result).isFalse(); assertThat(options.inPreferredConfig).isNotEqualTo(Bitmap.Config.HARDWARE); } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withInvalidWidth_returnsFalse_doesNotSetValues() { HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ -1, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertThat(result).isFalse(); assertThat(options.inMutable).isTrue(); assertThat(options.inPreferredConfig).isNull(); } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withInvalidHeight_returnsFalse_doesNotSetValues() { HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ -1, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertThat(result).isFalse(); assertThat(options.inMutable).isTrue(); assertThat(options.inPreferredConfig).isNull(); } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withHardwareConfigDisallowed_returnsFalse_doesNotSetValues() { HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ false, /* isExifOrientationRequired= */ false); assertThat(result).isFalse(); assertThat(options.inMutable).isTrue(); assertThat(options.inPreferredConfig).isNull(); } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withExifOrientationRequired_returnsFalse_doesNotSetValues() { HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ true); assertThat(result).isFalse(); assertThat(options.inMutable).isTrue(); assertThat(options.inPreferredConfig).isNull(); } @Config(sdk = Build.VERSION_CODES.N_MR1) @Test public void setHardwareConfigIfAllowed_withOsLessThanO_returnsFalse_doesNotSetValues() { HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertThat(result).isFalse(); assertThat(options.inMutable).isTrue(); assertThat(options.inPreferredConfig).isNull(); } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withOsLessThanQ_beforeUnblockingHardwareBitmaps_returnsFalseAndDoesNotSetValues() { HardwareConfigState state = new HardwareConfigState(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertThat(result).isFalse(); assertThat(options.inMutable).isTrue(); assertThat(options.inPreferredConfig).isNull(); } @Config(sdk = Build.VERSION_CODES.Q) @Test public void setHardwareConfigIfAllowed_withOsQ_beforeUnblockingHardwareBitmaps_returnsTrueAndSetsValues() { HardwareConfigState state = new HardwareConfigState(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertThat(result).isTrue(); assertThat(options.inMutable).isFalse(); assertThat(options.inPreferredConfig).isEqualTo(Bitmap.Config.HARDWARE); } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withPreviouslyDisallowedSamsungDevices_P_returnsTrue() { for (String model : new String[] { "SM-N9351", "SM-J72053", "SM-G9600", "SM-G965ab", "SM-G935.", "SM-G930", "SM-A5204" }) { ShadowBuild.setModel(model); HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertWithMessage("model: " + model).that(result).isTrue(); assertWithMessage("model: " + model).that(options.inMutable).isFalse(); assertWithMessage("model: " + model) .that(options.inPreferredConfig) .isEqualTo(Bitmap.Config.HARDWARE); } } @Config(sdk = Build.VERSION_CODES.P) @Test public void setHardwareConfigIfAllowed_withShortOrEmptyModelNames_returnsTrue() { for (String model : new String[] {".", "-", "", "S", "SM", "SM-", "SM-G", "SM-G9.", "SM-G93"}) { ShadowBuild.setModel(model); HardwareConfigState state = new HardwareConfigState(); state.unblockHardwareBitmaps(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = null; options.inMutable = true; boolean result = state.setHardwareConfigIfAllowed( /* targetWidth= */ VALID_DIMENSION, /* targetHeight= */ VALID_DIMENSION, options, /* isHardwareConfigAllowed= */ true, /* isExifOrientationRequired= */ false); assertWithMessage("model: " + model).that(result).isTrue(); assertWithMessage("model: " + model).that(options.inMutable).isFalse(); assertWithMessage("model: " + model) .that(options.inPreferredConfig) .isEqualTo(Bitmap.Config.HARDWARE); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/LazyBitmapDrawableResourceTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.engine.Initializable; import com.bumptech.glide.load.engine.Resource; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class LazyBitmapDrawableResourceTest { @Mock private Resource bitmapResource; private LazyBitmapDrawableResource resource; private Resources resources; private Bitmap bitmap; @Before public void setUp() { MockitoAnnotations.initMocks(this); bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(bitmapResource.get()).thenReturn(bitmap); resources = ApplicationProvider.getApplicationContext().getResources(); resource = (LazyBitmapDrawableResource) LazyBitmapDrawableResource.obtain(resources, bitmapResource); } @Test public void obtain_withNullBitmapResource_returnsNull() { assertThat(LazyBitmapDrawableResource.obtain(resources, null)).isNull(); } @Test public void getSize_returnsSizeOfWrappedResource() { when(bitmapResource.getSize()).thenReturn(100); assertThat(resource.getSize()).isEqualTo(100); } @Test public void recycle_callsRecycleOnWrappedResource() { resource.recycle(); verify(bitmapResource).recycle(); } @Test public void recycle_doesNotRecycleWrappedBitmap() { resource.recycle(); assertThat(bitmap.isRecycled()).isFalse(); } @Test public void get_returnsDrawableContainingWrappedBitmap() { BitmapDrawable drawable = resource.get(); assertThat(drawable.getBitmap()).isSameInstanceAs(bitmap); } @Test public void initialize_withNonInitializableResource_doesNothing() { resource.initialize(); } @Test public void initialize_withWrappedInitializableResource_callsInitializeOnWrapped() { InitializableBitmapResource bitmapResource = mock(InitializableBitmapResource.class); resource = (LazyBitmapDrawableResource) LazyBitmapDrawableResource.obtain(resources, bitmapResource); resource.initialize(); verify(bitmapResource).initialize(); } private interface InitializableBitmapResource extends Initializable, Resource { // Intentionally empty. } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/RecyclableBufferedInputStreamTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; // Not required in tests. @SuppressWarnings("ResultOfMethodCallIgnored") @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class RecyclableBufferedInputStreamTest { private static final int DATA_SIZE = 30; private static final int BUFFER_SIZE = 10; private RecyclableBufferedInputStream stream; private byte[] data; private ArrayPool byteArrayPool; @Before public void setUp() { data = new byte[DATA_SIZE]; for (int i = 0; i < DATA_SIZE; i++) { data[i] = (byte) i; } byteArrayPool = new LruArrayPool(); InputStream wrapped = new ByteArrayInputStream(data); stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool, BUFFER_SIZE); } @Test public void testReturnsTrueForMarkSupported() { assertTrue(stream.markSupported()); } @Test public void testCanReadIndividualBytes() throws IOException { for (int i = 0; i < data.length; i++) { assertEquals(i, stream.read()); } assertEquals(-1, stream.read()); } @Test public void testCanReadBytesInBulkLargerThanBufferSize() throws IOException { byte[] buffer = new byte[DATA_SIZE]; assertEquals(DATA_SIZE, stream.read(buffer, 0, DATA_SIZE)); for (int i = 0; i < DATA_SIZE; i++) { assertEquals(i, buffer[i]); } } @Test public void testCanReadBytesInBulkSmallerThanBufferSize() throws IOException { int toRead = BUFFER_SIZE / 2; byte[] buffer = new byte[toRead]; assertEquals(toRead, stream.read(buffer, 0, toRead)); for (int i = 0; i < toRead; i++) { assertEquals(i, buffer[i]); } } @Test public void testReadingZeroBytesIntoBufferReadsZeroBytes() throws IOException { // Make sure the next value is not 0. stream.read(); byte[] buffer = new byte[BUFFER_SIZE]; assertEquals(0, stream.read(buffer, 0, 0)); for (int i = 0; i < BUFFER_SIZE; i++) { assertEquals(0, buffer[i]); } } @Test public void testCanReadIntoBufferLargerThanDataSize() throws IOException { int toRead = DATA_SIZE * 2; byte[] buffer = new byte[toRead]; assertEquals(DATA_SIZE, stream.read(buffer, 0, toRead)); for (int i = 0; i < DATA_SIZE; i++) { assertEquals(i, buffer[i]); } for (int i = DATA_SIZE; i < toRead; i++) { assertEquals(0, buffer[i]); } } @Test public void testCanReadBytesInBulkWithLimit() throws IOException { int toRead = BUFFER_SIZE / 2; byte[] buffer = new byte[BUFFER_SIZE]; assertEquals(toRead, stream.read(buffer, 0, toRead)); // 0, 1, 2, 3, 4, 0, 0, 0, 0, 0 for (int i = 0; i < toRead; i++) { assertEquals(i, buffer[i]); } for (int i = toRead; i < BUFFER_SIZE; i++) { assertEquals(0, buffer[i]); } } @Test public void testCanReadBytesInBulkWithOffset() throws IOException { int toRead = BUFFER_SIZE / 2; byte[] buffer = new byte[BUFFER_SIZE]; assertEquals(toRead, stream.read(buffer, BUFFER_SIZE - toRead, toRead)); // 0, 0, 0, 0, 0, 0, 1, 2, 3, 4 for (int i = 0; i < toRead; i++) { assertEquals(0, buffer[i]); } for (int i = toRead; i < BUFFER_SIZE; i++) { assertEquals(i - toRead, buffer[i]); } } @Test public void testCanReadBytesInBulkWhenSomeButNotAllBytesAreInBuffer() throws IOException { stream.read(); byte[] buffer = new byte[BUFFER_SIZE]; assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE)); for (int i = 1; i < BUFFER_SIZE + 1; i++) { assertEquals(i, buffer[i - 1]); } } @Test public void testCanSkipBytes() throws IOException { int toSkip = data.length / 2; assertEquals(toSkip, stream.skip(toSkip)); for (int i = toSkip; i < data.length; i++) { assertEquals(i, stream.read()); } assertEquals(-1, stream.read()); } @Test public void testSkipReturnsZeroIfSkipByteCountIsZero() throws IOException { assertEquals(0, stream.skip(0)); assertEquals(0, stream.read()); } @Test public void testSkipReturnsZeroIfSkipByteCountIsNegative() throws IOException { assertEquals(0, stream.skip(-13)); assertEquals(0, stream.read()); } @Test public void testCloseClosesWrappedStream() throws IOException { InputStream wrapped = mock(InputStream.class); stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool); stream.close(); verify(wrapped).close(); } @Test public void testCanSafelyBeClosedMultipleTimes() throws IOException { InputStream wrapped = mock(InputStream.class); stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool); stream.close(); stream.close(); stream.close(); verify(wrapped, times(1)).close(); } @Test public void testCanMarkAndReset() throws IOException { byte[] buffer = new byte[BUFFER_SIZE]; stream.mark(BUFFER_SIZE); assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE)); for (int i = 0; i < BUFFER_SIZE; i++) { assertEquals(i, buffer[i]); } Arrays.fill(buffer, (byte) 0); stream.reset(); assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE)); for (int i = 0; i < BUFFER_SIZE; i++) { assertEquals(i, buffer[i]); } } @Test public void testCanResetRepeatedlyAfterMarking() throws IOException { byte[] buffer = new byte[BUFFER_SIZE]; stream.mark(BUFFER_SIZE); for (int repeat = 0; repeat < 10; repeat++) { assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE)); for (int i = 0; i < BUFFER_SIZE; i++) { assertEquals(i, buffer[i]); } stream.reset(); } } @Test public void testCanMarkInMiddleOfBufferAndStillReadUpToBufferLengthBeforeResetting() throws IOException { int markPos = BUFFER_SIZE / 2; for (int i = 0; i < markPos; i++) { stream.read(); } stream.mark(BUFFER_SIZE); for (int i = 0; i < BUFFER_SIZE; i++) { stream.read(); } stream.reset(); assertEquals(markPos, stream.read()); } @Test public void testAvailableReturnsWrappedAvailableIfNoBytesRead() throws IOException { assertEquals(DATA_SIZE, stream.available()); } @Test public void testAvailableIncludesDataInBufferAndWrappedAvailableIfBytesRead() throws IOException { stream.read(); assertEquals(DATA_SIZE - 1, stream.available()); } @Test(expected = IOException.class) public void testCloseThrowsIfWrappedStreamThrowsOnClose() throws IOException { InputStream wrapped = mock(InputStream.class); doThrow(new IOException()).when(wrapped).close(); stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool); stream.close(); } @Test(expected = IOException.class) public void testAvailableThrowsIfStreamIsClosed() throws IOException { stream.close(); stream.available(); } @Test(expected = IOException.class) public void testReadThrowsIfStreamIsClosed() throws IOException { stream.close(); stream.read(); } @Test(expected = IOException.class) public void testReadBulkThrowsIfStreamIsClosed() throws IOException { stream.close(); stream.read(new byte[1], 0, 1); } @Test(expected = IOException.class) public void testResetThrowsIfStreamIsClosed() throws IOException { stream.close(); stream.reset(); } @Test(expected = IOException.class) public void testSkipThrowsIfStreamIsClosed() throws IOException { stream.close(); stream.skip(10); } @Test(expected = RecyclableBufferedInputStream.InvalidMarkException.class) public void testResetThrowsIfMarkNotSet() throws IOException { stream.reset(); } @Test(expected = RecyclableBufferedInputStream.InvalidMarkException.class) public void testResetThrowsIfMarkIsInvalid() throws IOException { stream.mark(1); stream.read(new byte[BUFFER_SIZE], 0, BUFFER_SIZE); stream.read(); stream.reset(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/TransformationUtilsTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.ColorSpace; import android.graphics.Matrix; import android.os.Build.VERSION_CODES; import androidx.exifinterface.media.ExifInterface; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.tests.Util; import com.google.common.collect.Range; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = 28) public class TransformationUtilsTest { @Mock private BitmapPool bitmapPool; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(bitmapPool.get(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenAnswer(new Util.CreateBitmap()); } @Test public void testFitCenterWithWideBitmap() { final int maxSide = 500; Bitmap wide = Bitmap.createBitmap(2000, 100, Bitmap.Config.ARGB_8888); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, wide, maxSide, maxSide); assertHasOriginalAspectRatio(wide, transformed); assertBitmapFitsExactlyWithinBounds(maxSide, transformed); } @Test public void testFitCenterWithSmallWideBitmap() { final int maxSide = 500; Bitmap smallWide = Bitmap.createBitmap(400, 40, Bitmap.Config.ARGB_8888); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, smallWide, maxSide, maxSide); assertHasOriginalAspectRatio(smallWide, transformed); assertBitmapFitsExactlyWithinBounds(maxSide, transformed); } @Test public void testFitCenterWithTallBitmap() { final int maxSide = 500; Bitmap tall = Bitmap.createBitmap(65, 3000, Bitmap.Config.ARGB_8888); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, tall, maxSide, maxSide); assertHasOriginalAspectRatio(tall, transformed); assertBitmapFitsExactlyWithinBounds(maxSide, transformed); } @Test public void testFitCenterWithSmallTallBitmap() { final int maxSide = 500; Bitmap smallTall = Bitmap.createBitmap(10, 400, Bitmap.Config.ARGB_8888); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, smallTall, maxSide, maxSide); assertHasOriginalAspectRatio(smallTall, transformed); assertBitmapFitsExactlyWithinBounds(maxSide, transformed); } @Test public void testFitCenterWithSquareBitmap() { final int maxSide = 500; Bitmap square = Bitmap.createBitmap(600, 600, Bitmap.Config.ARGB_8888); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, square, maxSide, maxSide); assertHasOriginalAspectRatio(square, transformed); assertBitmapFitsExactlyWithinBounds(maxSide, transformed); } @Test public void testFitCenterWithTooSmallSquareBitmap() { final int maxSide = 500; Bitmap smallSquare = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, smallSquare, maxSide, maxSide); assertHasOriginalAspectRatio(smallSquare, transformed); assertBitmapFitsExactlyWithinBounds(maxSide, transformed); } // Test for Issue #195. @Test public void testFitCenterUsesFloorInsteadOfRoundingForOutputBitmapSize() { Bitmap toTransform = Bitmap.createBitmap(1230, 1640, Bitmap.Config.RGB_565); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, toTransform, 1075, 1366); assertEquals(1024, transformed.getWidth()); assertEquals(1366, transformed.getHeight()); } @Test public void testFitCenterReturnsGivenBitmapIfGivenBitmapMatchesExactly() { Bitmap toFit = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, toFit, toFit.getWidth(), toFit.getHeight()); assertTrue(toFit == transformed); } @Test public void testFitCenterReturnsGivenBitmapIfGivenBitmapWidthMatchesExactly() { Bitmap toFit = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, toFit, toFit.getWidth(), toFit.getHeight() * 2); assertTrue(toFit == transformed); } @Test public void testFitCenterReturnsGivenBitmapIfGivenBitmapHeightMatchesExactly() { Bitmap toFit = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, toFit, toFit.getWidth() * 2, toFit.getHeight()); assertTrue(toFit == transformed); } @Test public void testCenterCropReturnsGivenBitmapIfGivenBitmapExactlyMatchesGivenDimensions() { Bitmap toCrop = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888); Bitmap transformed = TransformationUtils.centerCrop(bitmapPool, toCrop, toCrop.getWidth(), toCrop.getHeight()); // Robolectric incorrectly implements equals() for Bitmaps, we want the original object not // just an equivalent. assertTrue(toCrop == transformed); } @Test @Config(sdk = 19) public void testFitCenterHandlesBitmapsWithNullConfigs() { Bitmap toFit = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); toFit.setConfig(null); Bitmap transformed = TransformationUtils.fitCenter(bitmapPool, toFit, 50, 50); assertEquals(Bitmap.Config.ARGB_8888, transformed.getConfig()); } @Test public void testCenterCropSetsOutBitmapToHaveAlphaIfInBitmapHasAlphaAndOutBitmapIsReused() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); reset(bitmapPool); when(bitmapPool.get(eq(50), eq(50), eq(Bitmap.Config.ARGB_8888))).thenReturn(toReuse); toReuse.setHasAlpha(false); toTransform.setHasAlpha(true); Bitmap result = TransformationUtils.centerCrop( bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight()); assertEquals(toReuse, result); assertTrue(result.hasAlpha()); } @Test public void testCenterCropSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlphaAndOutBitmapIsReused() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); reset(bitmapPool); when(bitmapPool.get(eq(50), eq(50), eq(Bitmap.Config.ARGB_8888))).thenReturn(toReuse); toReuse.setHasAlpha(true); toTransform.setHasAlpha(false); Bitmap result = TransformationUtils.centerCrop( bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight()); assertEquals(toReuse, result); assertFalse(result.hasAlpha()); } @Test public void testCenterCropSetsOutBitmapToHaveAlphaIfInBitmapHasAlpha() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); toTransform.setHasAlpha(true); Bitmap result = TransformationUtils.centerCrop( bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2); assertTrue(result.hasAlpha()); } @Test @Config(sdk = 19) public void testCenterCropHandlesBitmapsWithNullConfigs() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); toTransform.setConfig(null); Bitmap transformed = TransformationUtils.centerCrop(bitmapPool, toTransform, 50, 50); assertEquals(Bitmap.Config.ARGB_8888, transformed.getConfig()); } @Test public void testCenterCropSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlpha() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); toTransform.setHasAlpha(false); Bitmap result = TransformationUtils.centerCrop( bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2); assertFalse(result.hasAlpha()); } @Test public void testFitCenterSetsOutBitmapToHaveAlphaIfInBitmapHasAlphaAndOutBitmapIsReused() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); reset(bitmapPool); when(bitmapPool.get(eq(toReuse.getWidth()), eq(toReuse.getHeight()), eq(toReuse.getConfig()))) .thenReturn(toReuse); toReuse.setHasAlpha(false); toTransform.setHasAlpha(true); Bitmap result = TransformationUtils.fitCenter( bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight()); assertEquals(toReuse, result); assertTrue(result.hasAlpha()); } @Test public void testFitCenterSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlphaAndOutBitmapIsReused() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Bitmap toReuse = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); reset(bitmapPool); when(bitmapPool.get(eq(toReuse.getWidth()), eq(toReuse.getHeight()), eq(toReuse.getConfig()))) .thenReturn(toReuse); toReuse.setHasAlpha(true); toTransform.setHasAlpha(false); Bitmap result = TransformationUtils.fitCenter( bitmapPool, toTransform, toReuse.getWidth(), toReuse.getHeight()); assertEquals(toReuse, result); assertFalse(result.hasAlpha()); } @Test public void testFitCenterSetsOutBitmapToHaveAlphaIfInBitmapHasAlpha() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); toTransform.setHasAlpha(true); Bitmap result = TransformationUtils.fitCenter( bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2); assertTrue(result.hasAlpha()); } @Test public void testFitCenterSetsOutBitmapToNotHaveAlphaIfInBitmapDoesNotHaveAlpha() { Bitmap toTransform = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); toTransform.setHasAlpha(false); Bitmap result = TransformationUtils.fitCenter( bitmapPool, toTransform, toTransform.getWidth() / 2, toTransform.getHeight() / 2); assertFalse(result.hasAlpha()); } private static void assertHasOriginalAspectRatio(Bitmap original, Bitmap transformed) { double originalAspectRatio = (double) original.getWidth() / (double) original.getHeight(); double transformedAspectRatio = (double) transformed.getWidth() / (double) transformed.getHeight(); assertThat(transformedAspectRatio) .isIn(Range.open(originalAspectRatio - 0.05f, originalAspectRatio + 0.05f)); } private static void assertBitmapFitsExactlyWithinBounds(int maxSide, Bitmap bitmap) { final int width = bitmap.getWidth(); final int height = bitmap.getHeight(); assertThat(width).isIn(Range.atMost(maxSide)); assertThat(height).isIn(Range.atMost(maxSide)); assertTrue("one side must match maxSide", width == maxSide || height == maxSide); } @Test public void testGetExifOrientationDegrees() { assertEquals( 0, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_NORMAL)); assertEquals( 90, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_TRANSPOSE)); assertEquals( 90, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_ROTATE_90)); assertEquals( 180, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_ROTATE_180)); assertEquals( 180, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_FLIP_VERTICAL)); assertEquals( 270, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_TRANSVERSE)); assertEquals( 270, TransformationUtils.getExifOrientationDegrees(ExifInterface.ORIENTATION_ROTATE_270)); } @Test public void testRotateImage() { Bitmap toRotate = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888); toRotate.setPixel(0, 0, Color.BLUE); toRotate.setPixel(0, 1, Color.RED); Bitmap zero = TransformationUtils.rotateImage(toRotate, 0); assertTrue(toRotate == zero); Bitmap ninety = TransformationUtils.rotateImage(toRotate, 90); // Checks if native graphics is enabled. if (System.getProperty("robolectric.graphicsMode", "").equals("NATIVE")) { assertThat(ninety.getPixel(0, 0)).isEqualTo(Color.RED); assertThat(ninety.getPixel(1, 0)).isEqualTo(Color.BLUE); } else { // Use legacy shadow APIs assertThat(Shadows.shadowOf(ninety).getDescription()).contains("rotate=90.0"); } assertEquals(toRotate.getWidth(), toRotate.getHeight()); } @Test public void testRotateImageExifReturnsGivenBitmapIfRotationIsNormal() { Bitmap toRotate = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444); // Use assertTrue because Robolectric incorrectly implements equality for Bitmaps. We want // not just an identical Bitmap, but our original Bitmap object back. Bitmap rotated = TransformationUtils.rotateImageExif(bitmapPool, toRotate, ExifInterface.ORIENTATION_NORMAL); assertTrue(toRotate == rotated); } @Test public void testRotateImageExifReturnsGivenBitmapIfRotationIsUndefined() { Bitmap toRotate = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); // Use assertTrue because Robolectric incorrectly implements equality for Bitmaps. We want // not just an identical Bitmap, but our original Bitmap object back. Bitmap rotated = TransformationUtils.rotateImageExif( bitmapPool, toRotate, ExifInterface.ORIENTATION_UNDEFINED); assertTrue(toRotate == rotated); } @Test public void testRotateImageExifReturnsGivenBitmapIfOrientationIsInvalid() { Bitmap toRotate = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888); // Use assertTrue because Robolectric incorrectly implements equality for Bitmaps. We want // not just an identical Bitmap, but our original Bitmap object back. Bitmap rotated = TransformationUtils.rotateImageExif(bitmapPool, toRotate, -1); assertTrue(toRotate == rotated); } @Test @Config(sdk = 19) public void testRotateImageExif_preservesitmapsWithNullConfigs() { Bitmap toRotate = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); toRotate.setConfig(null); Bitmap rotated = TransformationUtils.rotateImageExif( bitmapPool, toRotate, ExifInterface.ORIENTATION_ROTATE_180); assertNull(rotated.getConfig()); } @Test @Config(sdk = VERSION_CODES.UPSIDE_DOWN_CAKE) public void rotateImageExif_preservesColorSpace() { Bitmap toRotate = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888); toRotate.setColorSpace(ColorSpace.get(ColorSpace.Named.DISPLAY_P3)); Bitmap rotated = TransformationUtils.rotateImageExif( bitmapPool, toRotate, ExifInterface.ORIENTATION_ROTATE_90); assertEquals(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), rotated.getColorSpace()); } // TODO: Add gainmap-based tests once Robolectric has sufficient support. @Test public void testInitializeMatrixSetsScaleIfFlipHorizontal() { Matrix matrix = mock(Matrix.class); TransformationUtils.initializeMatrixForRotation( ExifInterface.ORIENTATION_FLIP_HORIZONTAL, matrix); verify(matrix).setScale(-1, 1); } @Test public void testInitializeMatrixSetsScaleAndRotateIfFlipVertical() { Matrix matrix = mock(Matrix.class); TransformationUtils.initializeMatrixForRotation( ExifInterface.ORIENTATION_FLIP_VERTICAL, matrix); verify(matrix).setRotate(180); verify(matrix).postScale(-1, 1); } @Test public void testInitializeMatrixSetsScaleAndRotateIfTranspose() { Matrix matrix = mock(Matrix.class); TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_TRANSPOSE, matrix); verify(matrix).setRotate(90); verify(matrix).postScale(-1, 1); } @Test public void testInitializeMatrixSetsScaleAndRotateIfTransverse() { Matrix matrix = mock(Matrix.class); TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_TRANSVERSE, matrix); verify(matrix).setRotate(-90); verify(matrix).postScale(-1, 1); } @Test public void testInitializeMatrixSetsRotateOnRotation() { Matrix matrix = mock(Matrix.class); TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_ROTATE_90, matrix); verify(matrix).setRotate(90); TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_ROTATE_180, matrix); verify(matrix).setRotate(180); TransformationUtils.initializeMatrixForRotation(ExifInterface.ORIENTATION_ROTATE_270, matrix); verify(matrix).setRotate(-90); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java ================================================ package com.bumptech.glide.load.resource.bitmap; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.ParcelFileDescriptor; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.tests.Util; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; @RunWith(RobolectricTestRunner.class) @Config(sdk = VERSION_CODES.O_MR1) public class VideoDecoderTest { @Mock private ParcelFileDescriptor resource; @Mock private VideoDecoder.MediaMetadataRetrieverFactory factory; @Mock private VideoDecoder.MediaInitializer initializer; @Mock private MediaMetadataRetriever retriever; @Mock private BitmapPool bitmapPool; private VideoDecoder decoder; private Options options; private int initialSdkVersion; private String initialMake; private String initialModel; private String initialBuildId; private String initialDevice; @Before public void setup() { MockitoAnnotations.initMocks(this); when(factory.build()).thenReturn(retriever); decoder = new VideoDecoder<>(bitmapPool, initializer, factory); options = new Options(); initialSdkVersion = Build.VERSION.SDK_INT; initialMake = Build.MANUFACTURER; initialModel = Build.MODEL; initialBuildId = Build.ID; initialDevice = Build.DEVICE; } @After public void tearDown() { Util.setSdkVersionInt(initialSdkVersion); resetBuildInfo(initialMake, initialModel, initialBuildId, initialDevice); } @Test public void testReturnsRetrievedFrameForResource() throws IOException { Util.setSdkVersionInt(19); Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(retriever.getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION)) .thenReturn(expected); Resource result = Preconditions.checkNotNull(decoder.decode(resource, 100, 100, options)); verify(initializer).initializeRetriever(retriever, resource); assertEquals(expected, result.get()); } @Test public void testReleasesMediaMetadataRetriever() { Util.setSdkVersionInt(19); assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() throws IOException { decoder.decode(resource, 1, 2, options); } }); try { verify(retriever).release(); } catch (Exception e) { // Ignore failures while cleaning up. } } @Test(expected = IllegalArgumentException.class) public void testThrowsExceptionIfCalledWithInvalidFrame() throws IOException { Util.setSdkVersionInt(19); options.set(VideoDecoder.TARGET_FRAME, -5L); new VideoDecoder<>(bitmapPool, initializer, factory).decode(resource, 100, 100, options); } @Test public void testSpecifiesThumbnailFrameIfICalledWithFrameNumber() { Util.setSdkVersionInt(19); long frame = 5; options.set(VideoDecoder.TARGET_FRAME, frame); decoder = new VideoDecoder<>(bitmapPool, initializer, factory); assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() throws IOException { decoder.decode(resource, 1, 2, options); } }); verify(retriever).getFrameAtTime(frame, VideoDecoder.DEFAULT_FRAME_OPTION); } @Test public void testDoesNotSpecifyThumbnailFrameIfCalledWithoutFrameNumber() { Util.setSdkVersionInt(19); decoder = new VideoDecoder<>(bitmapPool, initializer, factory); assertThrows( RuntimeException.class, new ThrowingRunnable() { @Override public void run() throws IOException { decoder.decode(resource, 100, 100, options); } }); verify(retriever).getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION); } @Test public void getScaledFrameAtTime() throws IOException { // Anything other than NONE. options.set(DownsampleStrategy.OPTION, DownsampleStrategy.AT_LEAST); Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)) .thenReturn("100"); when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)) .thenReturn("100"); when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)) .thenReturn("0"); when(retriever.getScaledFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, 100, 100)) .thenReturn(expected); assertThat(decoder.decode(resource, 100, 100, options).get()).isSameInstanceAs(expected); } @Test public void decodeFrame_withTargetSizeOriginal_onApi27_doesNotThrow() throws IOException { Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)) .thenReturn(expected); verify(retriever, never()).getScaledFrameAtTime(anyLong(), anyInt(), anyInt(), anyInt()); assertThat(decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get()) .isSameInstanceAs(expected); } @Test public void decodeFrame_withTargetSizeOriginalWidthOnly_onApi27_doesNotThrow() throws IOException { Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)) .thenReturn(expected); verify(retriever, never()).getScaledFrameAtTime(anyLong(), anyInt(), anyInt(), anyInt()); assertThat(decoder.decode(resource, Target.SIZE_ORIGINAL, 100, options).get()) .isSameInstanceAs(expected); } @Test public void decodeFrame_withTargetSizeOriginalHeightOnly_onApi27_doesNotThrow() throws IOException { Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)) .thenReturn(expected); verify(retriever, never()).getScaledFrameAtTime(anyLong(), anyInt(), anyInt(), anyInt()); assertThat(decoder.decode(resource, 100, Target.SIZE_ORIGINAL, options).get()) .isSameInstanceAs(expected); } @Test public void decodeFrame_notArcDeviceButWebm_doesNotInitializeMediaExtractor() throws IOException { setDevice("notArc"); when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)) .thenReturn("video/webm"); when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)) .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get(); verify(initializer, never()).initializeExtractor(any(), any()); } @Test public void decodeFrame_arcDeviceButNotWebm_doesNotInitializeMediaExtractor() throws IOException { setDevice("arc_cheets"); when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)) .thenReturn("video/mp4"); when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)) .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get(); verify(initializer, never()).initializeExtractor(any(), any()); } @Test public void decodeFrame_arcDeviceAndWebm_initializesMediaExtractor() throws IOException { setDevice("arc_cheets"); when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)) .thenReturn("video/webm"); when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)) .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get(); verify(initializer).initializeExtractor(any(), any()); } @Test @Config(sdk = VERSION_CODES.M) public void isHdr180RotationFixRequired_androidM_returnsFalse() { assertThat(VideoDecoder.isHdr180RotationFixRequired()).isFalse(); } @Test @Config(sdk = VERSION_CODES.Q) public void isHdr180RotationFixRequired_androidQ_returnsFalse() { assertThat(VideoDecoder.isHdr180RotationFixRequired()).isFalse(); } @Test @Config(sdk = VERSION_CODES.R) public void isHdr180RotationFixRequired_androidR_returnsTrue() { assertThat(VideoDecoder.isHdr180RotationFixRequired()).isTrue(); } @Test @Config(sdk = VERSION_CODES.S) public void isHdr180RotationFixRequired_androidS_returnsTrue() { assertThat(VideoDecoder.isHdr180RotationFixRequired()).isTrue(); } private void resetBuildInfo(String make, String model, String buildId, String device) { ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", make); ReflectionHelpers.setStaticField(Build.class, "MODEL", model); ReflectionHelpers.setStaticField(Build.class, "ID", buildId); setDevice(device); } private void setDevice(String device) { ReflectionHelpers.setStaticField(Build.class, "DEVICE", device); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/bytes/BytesResourceTest.java ================================================ package com.bumptech.glide.load.resource.bytes; import static org.junit.Assert.assertEquals; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class BytesResourceTest { @Test public void testReturnsGivenBytes() { byte[] bytes = new byte[0]; BytesResource resource = new BytesResource(bytes); assertEquals(bytes, resource.get()); } @Test public void testReturnsSizeOfGivenBytes() { byte[] bytes = new byte[123]; BytesResource resource = new BytesResource(bytes); assertEquals(bytes.length, resource.getSize()); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullBytes() { new BytesResource(null); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/drawable/DrawableResourceTest.java ================================================ package com.bumptech.glide.load.resource.drawable; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNotEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.load.resource.gif.GifDrawable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class DrawableResourceTest { private TestDrawable drawable; private DrawableResource resource; @Before public void setUp() { drawable = mock(TestDrawable.class); resource = new DrawableResource(drawable) { @NonNull @Override public Class getResourceClass() { return TestDrawable.class; } @Override public int getSize() { return 0; } @Override public void recycle() {} }; } @Test public void testDoesNotReturnOriginalDrawableOnGet() { when(drawable.getConstantState()).thenReturn(mock(Drawable.ConstantState.class)); assertNotEquals(drawable, resource.get()); } @SuppressWarnings("TruthIncompatibleType") @Test public void testReturnsNewDrawableOnGet() { GifDrawable expected = mock(GifDrawable.class); Drawable.ConstantState constantState = mock(Drawable.ConstantState.class); when(constantState.newDrawable()).thenReturn(expected); when(drawable.getConstantState()).thenReturn(constantState); assertThat(resource.get()) .isEqualTo(/* expected: TestDrawable, actual: GifDrawable */ expected); verify(drawable).getConstantState(); verify(constantState).newDrawable(); } @Test public void get_withNullState_returnsOriginalDrawable() { when(drawable.getConstantState()).thenReturn(null); assertThat(resource.get()).isEqualTo(drawable); } @Test(expected = NullPointerException.class) public void testThrowsIfDrawableIsNull() { new DrawableResource(null) { @NonNull @Override public Class getResourceClass() { return TestDrawable.class; } @Override public int getSize() { return 0; } @Override public void recycle() {} }; } /** Just to have a type to test with which is not directly Drawable */ private static class TestDrawable extends Drawable { @Override public void draw(@NonNull Canvas canvas) {} @Override public void setAlpha(int alpha) {} @Override public void setColorFilter(ColorFilter cf) {} @Override public int getOpacity() { return 0; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/file/FileDecoderTest.java ================================================ package com.bumptech.glide.load.resource.file; import static org.junit.Assert.assertEquals; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.Preconditions; import java.io.File; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class FileDecoderTest { private FileDecoder decoder; private Options options; @Before public void setUp() { decoder = new FileDecoder(); options = new Options(); } @Test public void testReturnsGivenFileAsResource() throws IOException { File expected = new File("testFile"); Resource decoded = Preconditions.checkNotNull(decoder.decode(expected, 1, 1, options)); assertEquals(expected, decoded.get()); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/file/FileResourceTest.java ================================================ package com.bumptech.glide.load.resource.file; import static org.junit.Assert.assertEquals; import java.io.File; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class FileResourceTest { private File file; private FileResource resource; @Before public void setUp() { file = new File("Test"); resource = new FileResource(file); } @Test public void testReturnsGivenFile() { assertEquals(file, resource.get()); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/gif/ByteBufferGifDecoderTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.gifdecoder.GifHeader; import com.bumptech.glide.gifdecoder.GifHeaderParser; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ByteBufferGifDecoderTest { private static final byte[] GIF_HEADER = new byte[] {0x47, 0x49, 0x46}; private static final int ARRAY_POOL_SIZE_BYTES = 4 * 1024 * 1024; private ByteBufferGifDecoder decoder; private GifHeader gifHeader; private Options options; @Mock private BitmapPool bitmapPool; @Mock private GifHeaderParser parser; @Mock private GifDecoder gifDecoder; @Mock private ByteBufferGifDecoder.GifHeaderParserPool parserPool; @Mock private ByteBufferGifDecoder.GifDecoderFactory decoderFactory; @Before public void setUp() { MockitoAnnotations.initMocks(this); gifHeader = Mockito.spy(new GifHeader()); when(parser.parseHeader()).thenReturn(gifHeader); when(parserPool.obtain(isA(ByteBuffer.class))).thenReturn(parser); when(decoderFactory.build( isA(GifDecoder.BitmapProvider.class), eq(gifHeader), isA(ByteBuffer.class), anyInt())) .thenReturn(gifDecoder); List parsers = new ArrayList<>(); parsers.add(new DefaultImageHeaderParser()); options = new Options(); decoder = new ByteBufferGifDecoder( ApplicationProvider.getApplicationContext(), parsers, bitmapPool, new LruArrayPool(ARRAY_POOL_SIZE_BYTES), parserPool, decoderFactory); } @Test public void testDoesNotHandleStreamIfEnabledButNotAGif() throws IOException { assertThat(decoder.handles(ByteBuffer.allocate(0), options)).isFalse(); } @Test public void testHandlesStreamIfContainsGifHeaderAndDisabledIsNotSet() throws IOException { assertThat(decoder.handles(ByteBuffer.wrap(GIF_HEADER), options)).isTrue(); } @Test public void testHandlesStreamIfContainsGifHeaderAndDisabledIsFalse() throws IOException { options.set(GifOptions.DISABLE_ANIMATION, false); assertThat(decoder.handles(ByteBuffer.wrap(GIF_HEADER), options)).isTrue(); } @Test public void testDoesNotHandleStreamIfDisabled() throws IOException { options.set(GifOptions.DISABLE_ANIMATION, true); assertThat(decoder.handles(ByteBuffer.wrap(GIF_HEADER), options)).isFalse(); } @Test public void testReturnsNullIfParsedHeaderHasZeroFrames() throws IOException { when(gifHeader.getNumFrames()).thenReturn(0); assertNull(decoder.decode(ByteBuffer.allocate(10), 100, 100, options)); } @Test public void testReturnsNullIfParsedHeaderHasFormatError() { when(gifHeader.getStatus()).thenReturn(GifDecoder.STATUS_FORMAT_ERROR); assertNull(decoder.decode(ByteBuffer.allocate(10), 100, 100, options)); } @Test public void testReturnsNullIfParsedHeaderHasOpenError() { when(gifHeader.getStatus()).thenReturn(GifDecoder.STATUS_OPEN_ERROR); assertNull(decoder.decode(ByteBuffer.allocate(10), 100, 100, options)); } @Test public void testReturnsParserToPool() throws IOException { decoder.decode(ByteBuffer.allocate(10), 100, 100, options); verify(parserPool).release(eq(parser)); } @Test public void testReturnsParserToPoolWhenParserThrows() { when(parser.parseHeader()).thenThrow(new RuntimeException("Test")); try { decoder.decode(ByteBuffer.allocate(10), 100, 100, options); fail("Failed to receive expected exception"); } catch (RuntimeException e) { // Expected. } verify(parserPool).release(eq(parser)); } @Test public void testReturnsNullIfGifDecoderFailsToDecodeFirstFrame() { when(gifHeader.getNumFrames()).thenReturn(1); when(gifHeader.getStatus()).thenReturn(GifDecoder.STATUS_OK); when(gifDecoder.getNextFrame()).thenReturn(null); assertNull(decoder.decode(ByteBuffer.allocate(10), 100, 100, options)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/gif/GifDrawableResourceTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GifDrawableResourceTest { private GifDrawable drawable; private GifDrawableResource resource; @Before public void setUp() { drawable = mock(GifDrawable.class); resource = new GifDrawableResource(drawable); } @Test public void testReturnsSizeFromDrawable() { final int size = 2134; when(drawable.getSize()).thenReturn(size); assertEquals(size, resource.getSize()); } @Test public void testStopsAndThenRecyclesDrawableWhenRecycled() { resource.recycle(); InOrder inOrder = inOrder(drawable); inOrder.verify(drawable).stop(); inOrder.verify(drawable).recycle(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/gif/GifDrawableTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Application; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.os.Build; import android.view.View; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.tests.TearDownGlide; import com.bumptech.glide.tests.Util; import com.bumptech.glide.util.Preconditions; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowCanvas; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GifDrawableTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private GifDrawable drawable; private int frameHeight; private int frameWidth; private Bitmap firstFrame; private int initialSdkVersion; @Mock private Drawable.Callback cb; @Mock private GifFrameLoader frameLoader; @Mock private Paint paint; @Mock private Transformation transformation; private Application context; private static Paint isAPaint() { return isA(Paint.class); } private static Rect isARect() { return isA(Rect.class); } @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); frameWidth = 120; frameHeight = 450; firstFrame = Bitmap.createBitmap(frameWidth, frameHeight, Bitmap.Config.RGB_565); drawable = new GifDrawable(frameLoader, paint); when(frameLoader.getWidth()).thenReturn(frameWidth); when(frameLoader.getHeight()).thenReturn(frameHeight); when(frameLoader.getCurrentFrame()).thenReturn(firstFrame); when(frameLoader.getCurrentIndex()).thenReturn(0); drawable.setCallback(cb); initialSdkVersion = Build.VERSION.SDK_INT; } @After public void tearDown() { Util.setSdkVersionInt(initialSdkVersion); } @Test public void testShouldDrawFirstFrameBeforeAnyFrameRead() { Canvas canvas = new Canvas(); drawable.draw(canvas); ShadowCanvas shadowCanvas = Shadow.extract(canvas); assertThat(shadowCanvas.getDescription()) .isEqualTo( "Bitmap (" + firstFrame.getWidth() + " x " + firstFrame.getHeight() + ") at (0,0) with height=0 and width=0"); } @Test public void testDoesDrawCurrentFrameIfOneIsAvailable() { Canvas canvas = mock(Canvas.class); Bitmap currentFrame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); when(frameLoader.getCurrentFrame()).thenReturn(currentFrame); drawable.draw(canvas); verify(canvas).drawBitmap(eq(currentFrame), (Rect) isNull(), isARect(), isAPaint()); verify(canvas, never()).drawBitmap(eq(firstFrame), (Rect) isNull(), isARect(), isAPaint()); } @Test public void testRequestsNextFrameOnStart() { drawable.setVisible(true, true); drawable.start(); verify(frameLoader).subscribe(eq(drawable)); } @Test public void testRequestsNextFrameOnStartWithoutCallToSetVisible() { drawable.start(); verify(frameLoader).subscribe(eq(drawable)); } @Test public void testDoesNotRequestNextFrameOnStartIfGotCallToSetVisibleWithVisibleFalse() { drawable.setVisible(false, false); drawable.start(); verify(frameLoader, never()).subscribe(eq(drawable)); } @Test public void testDoesNotRequestNextFrameOnStartIfHasSingleFrame() { when(frameLoader.getFrameCount()).thenReturn(1); drawable.setVisible(true, false); drawable.start(); verify(frameLoader, never()).subscribe(eq(drawable)); } @Test public void testInvalidatesSelfOnStartIfHasSingleFrame() { when(frameLoader.getFrameCount()).thenReturn(1); drawable.setVisible(true, false); drawable.start(); verify(cb).invalidateDrawable(eq(drawable)); } @Test public void testShouldInvalidateSelfOnRun() { drawable.setVisible(true, true); drawable.start(); verify(cb).invalidateDrawable(eq(drawable)); } @Test public void testShouldNotScheduleItselfIfAlreadyRunning() { drawable.setVisible(true, true); drawable.start(); drawable.start(); verify(frameLoader, times(1)).subscribe(eq(drawable)); } @Test public void testReturnsFalseFromIsRunningWhenNotRunning() { assertFalse(drawable.isRunning()); } @Test public void testReturnsTrueFromIsRunningWhenRunning() { drawable.setVisible(true, true); drawable.start(); assertTrue(drawable.isRunning()); } @Test public void testInvalidatesSelfWhenFrameReady() { drawable.setIsRunning(true); drawable.onFrameReady(); verify(cb).invalidateDrawable(eq(drawable)); } @Test public void testDoesNotStartLoadingNextFrameWhenCurrentFinishesIfHasNoCallback() { drawable.setIsRunning(true); drawable.setCallback(null); drawable.onFrameReady(); verify(frameLoader).unsubscribe(eq(drawable)); } @Test public void testStopsWhenCurrentFrameFinishesIfHasNoCallback() { drawable.setIsRunning(true); drawable.setCallback(null); drawable.onFrameReady(); assertFalse(drawable.isRunning()); } @Test public void testUnsubscribesWhenCurrentFinishesIfHasNoCallback() { drawable.setIsRunning(true); drawable.setCallback(null); drawable.onFrameReady(); verify(frameLoader).unsubscribe(eq(drawable)); } @Test public void testSetsIsRunningFalseOnStop() { drawable.start(); drawable.stop(); assertFalse(drawable.isRunning()); } @Test public void testStopsOnSetVisibleFalse() { drawable.start(); drawable.setVisible(false, true); assertFalse(drawable.isRunning()); } @Test public void testStartsOnSetVisibleTrueIfRunning() { drawable.start(); drawable.setVisible(false, false); drawable.setVisible(true, true); assertTrue(drawable.isRunning()); } @Test public void testDoesNotStartOnVisibleTrueIfNotRunning() { drawable.setVisible(true, true); assertFalse(drawable.isRunning()); } @Test public void testDoesNotStartOnSetVisibleIfStartedAndStopped() { drawable.start(); drawable.stop(); drawable.setVisible(true, true); assertFalse(drawable.isRunning()); } @Test public void testDoesNotImmediatelyRunIfStartedWhileNotVisible() { drawable.setVisible(false, false); drawable.start(); assertFalse(drawable.isRunning()); } @Test public void testGetOpacityReturnsTransparent() { assertEquals(PixelFormat.TRANSPARENT, drawable.getOpacity()); } @Test public void testReturnsFrameCountFromDecoder() { int expected = 4; when(frameLoader.getFrameCount()).thenReturn(expected); assertEquals(expected, drawable.getFrameCount()); } @Test public void testReturnsDefaultFrameIndex() { final int expected = -1; when(frameLoader.getCurrentIndex()).thenReturn(expected); assertEquals(expected, drawable.getFrameIndex()); } @Test public void testReturnsNonDefaultFrameIndex() { final int expected = 100; when(frameLoader.getCurrentIndex()).thenReturn(expected); assertEquals(expected, drawable.getFrameIndex()); } @Test public void testRecycleCallsClearOnFrameManager() { drawable.recycle(); verify(frameLoader).clear(); } @Test public void testIsNotRecycledIfNotRecycled() { assertFalse(drawable.isRecycled()); } @Test public void testIsRecycledAfterRecycled() { drawable.recycle(); assertTrue(drawable.isRecycled()); } @Test public void testReturnsNonNullConstantState() { assertNotNull(drawable.getConstantState()); } @Test public void testReturnsSizeFromFrameLoader() { int size = 1243; when(frameLoader.getSize()).thenReturn(size); assertThat(drawable.getSize()).isEqualTo(size); } @Test public void testReturnsNewDrawableFromConstantState() { Bitmap firstFrame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); drawable = new GifDrawable( ApplicationProvider.getApplicationContext(), mock(GifDecoder.class), transformation, 100, 100, firstFrame); assertNotNull(Preconditions.checkNotNull(drawable.getConstantState()).newDrawable()); assertNotNull( drawable .getConstantState() .newDrawable(ApplicationProvider.getApplicationContext().getResources())); } @Test public void testReturnsFrameWidthAndHeightForIntrinsicDimensions() { assertEquals(frameWidth, drawable.getIntrinsicWidth()); assertEquals(frameHeight, drawable.getIntrinsicHeight()); } @Test public void testLoopsASingleTimeIfLoopCountIsSetToOne() { final int loopCount = 1; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(loopCount); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testLoopsForeverIfLoopCountIsSetToLoopForever() { final int loopCount = 40; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(GifDrawable.LOOP_FOREVER); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertTrue("drawable should be still running", drawable.isRunning()); } @Test public void testLoopsOnceIfLoopCountIsSetToOneWithThreeFrames() { final int loopCount = 1; final int frameCount = 3; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(loopCount); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testLoopsThreeTimesIfLoopCountIsSetToThree() { final int loopCount = 3; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(loopCount); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testCallingStartResetsLoopCounter() { when(frameLoader.getFrameCount()).thenReturn(2); drawable.setLoopCount(1); drawable.setVisible(true, true); drawable.start(); drawable.onFrameReady(); when(frameLoader.getCurrentIndex()).thenReturn(1); drawable.onFrameReady(); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); drawable.start(); when(frameLoader.getCurrentIndex()).thenReturn(0); drawable.onFrameReady(); when(frameLoader.getCurrentIndex()).thenReturn(1); drawable.onFrameReady(); // 4 onFrameReady(), 2 start() verify(cb, times(4 + 2)).invalidateDrawable(eq(drawable)); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testChangingTheLoopCountAfterHittingTheMaxLoopCount() { final int initialLoopCount = 1; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(initialLoopCount); drawable.setVisible(true, true); drawable.start(); runLoops(initialLoopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); final int newLoopCount = 2; drawable.setLoopCount(newLoopCount); drawable.start(); runLoops(newLoopCount, frameCount); int numStarts = 2; int expectedFrames = (initialLoopCount + newLoopCount) * frameCount + numStarts; verify(cb, times(expectedFrames)).invalidateDrawable(eq(drawable)); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfGivenLoopCountLessThanZeroAndNotInfinite() { drawable.setLoopCount(-2); } @Test public void testUsesDecoderTotalLoopCountIfLoopCountIsLoopIntrinsic() { final int frameCount = 3; final int loopCount = 2; when(frameLoader.getLoopCount()).thenReturn(loopCount); when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(GifDrawable.LOOP_INTRINSIC); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testLoopsForeverIfLoopCountIsLoopIntrinsicAndTotalIterationCountIsForever() { final int frameCount = 3; final int loopCount = 40; when(frameLoader.getLoopCount()).thenReturn(GifDecoder.TOTAL_ITERATION_COUNT_FOREVER); when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(GifDrawable.LOOP_INTRINSIC); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertTrue("drawable should be still running", drawable.isRunning()); } @Test public void testDoesNotDrawFrameAfterRecycle() { Bitmap bitmap = Bitmap.createBitmap(100, 112341, Bitmap.Config.RGB_565); drawable.setVisible(true, true); drawable.start(); when(frameLoader.getCurrentFrame()).thenReturn(bitmap); drawable.onFrameReady(); drawable.recycle(); Canvas canvas = mock(Canvas.class); drawable.draw(canvas); verify(canvas, never()).drawBitmap(eq(bitmap), isARect(), isARect(), isAPaint()); } @Test public void testSetsFrameTransformationOnFrameManager() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); drawable.setFrameTransformation(transformation, bitmap); verify(frameLoader).setFrameTransformation(eq(transformation), eq(bitmap)); } @Test(expected = NullPointerException.class) public void testThrowsIfConstructedWithNullFirstFrame() { new GifDrawable( ApplicationProvider.getApplicationContext(), mock(GifDecoder.class), transformation, 100, 100, null); } @Test public void testAppliesGravityOnDrawAfterBoundsChange() { Rect bounds = new Rect(0, 0, frameWidth * 2, frameHeight * 2); drawable.setBounds(bounds); Canvas canvas = mock(Canvas.class); drawable.draw(canvas); verify(canvas).drawBitmap(isA(Bitmap.class), (Rect) isNull(), eq(bounds), eq(paint)); } @Test public void testSetAlphaSetsAlphaOnPaint() { int alpha = 100; drawable.setAlpha(alpha); verify(paint).setAlpha(eq(alpha)); } @Test public void testSetColorFilterSetsColorFilterOnPaint() { ColorFilter colorFilter = new PorterDuffColorFilter(Color.RED, Mode.ADD); drawable.setColorFilter(colorFilter); // Use ArgumentCaptor instead of eq() due to b/73121412 where ShadowPorterDuffColorFilter.equals // uses a method that can't be found (PorterDuffColorFilter.getColor). ArgumentCaptor captor = ArgumentCaptor.forClass(ColorFilter.class); verify(paint).setColorFilter(captor.capture()); assertThat(captor.getValue()).isSameInstanceAs(colorFilter); } @Test public void testReturnsCurrentTransformationInGetFrameTransformation() { @SuppressWarnings("unchecked") Transformation newTransformation = mock(Transformation.class); Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); drawable.setFrameTransformation(newTransformation, bitmap); verify(frameLoader).setFrameTransformation(eq(newTransformation), eq(bitmap)); } @Test(expected = NullPointerException.class) public void testThrowsIfCreatedWithNullState() { new GifDrawable(null); } @Test public void onFrameReady_whenAttachedToDrawableCallbackButNotViewCallback_stops() { TransitionDrawable topLevel = new TransitionDrawable(new Drawable[] {drawable}); drawable.setCallback(topLevel); topLevel.setCallback(null); drawable.start(); drawable.onFrameReady(); assertThat(drawable.isRunning()).isFalse(); } @Test public void onFrameReady_whenAttachedtoDrawableCallbackWithViewCallbackParent_doesNotStop() { TransitionDrawable topLevel = new TransitionDrawable(new Drawable[] {drawable}); drawable.setCallback(topLevel); topLevel.setCallback(new View(context)); drawable.start(); drawable.onFrameReady(); assertThat(drawable.isRunning()).isTrue(); } private void verifyRanLoops(int loopCount, int frameCount) { // 1 for invalidate in start(). verify(cb, times(1 + loopCount * frameCount)).invalidateDrawable(eq(drawable)); } private void runLoops(int loopCount, int frameCount) { for (int loop = 0; loop < loopCount; loop++) { for (int frame = 0; frame < frameCount; frame++) { when(frameLoader.getCurrentIndex()).thenReturn(frame); assertTrue( "drawable should be started before calling drawable.onFrameReady()", drawable.isRunning()); drawable.onFrameReady(); } } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/gif/GifDrawableTransformationTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.mockResource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.UnitTransformation; import com.bumptech.glide.tests.KeyTester; import com.bumptech.glide.tests.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GifDrawableTransformationTest { @Rule public final KeyTester keyTester = new KeyTester(); @Mock private Transformation wrapped; @Mock private BitmapPool bitmapPool; private GifDrawableTransformation transformation; private Context context; @Before public void setUp() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool)); transformation = new GifDrawableTransformation(wrapped); } @After public void tearDown() { Glide.tearDown(); } @Test @SuppressWarnings("unchecked") public void testSetsTransformationAsFrameTransformation() { Resource resource = mockResource(); GifDrawable gifDrawable = mock(GifDrawable.class); Transformation unitTransformation = UnitTransformation.get(); when(gifDrawable.getFrameTransformation()).thenReturn(unitTransformation); when(gifDrawable.getIntrinsicWidth()).thenReturn(500); when(gifDrawable.getIntrinsicHeight()).thenReturn(500); when(resource.get()).thenReturn(gifDrawable); Bitmap firstFrame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(gifDrawable.getFirstFrame()).thenReturn(firstFrame); final int width = 123; final int height = 456; Bitmap expectedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Resource expectedResource = mockResource(); when(expectedResource.get()).thenReturn(expectedBitmap); when(wrapped.transform(any(Context.class), Util.anyResource(), anyInt(), anyInt())) .thenReturn(expectedResource); transformation.transform(context, resource, width, height); verify(gifDrawable).setFrameTransformation(isA(Transformation.class), eq(expectedBitmap)); } @Test public void testEquals() throws NoSuchAlgorithmException { doAnswer(new Util.WriteDigest("first")) .when(wrapped) .updateDiskCacheKey(isA(MessageDigest.class)); @SuppressWarnings("unchecked") Transformation other = mock(Transformation.class); doAnswer(new Util.WriteDigest("other")) .when(other) .updateDiskCacheKey(isA(MessageDigest.class)); keyTester .addEquivalenceGroup( transformation, new GifDrawableTransformation(wrapped), new GifDrawableTransformation(wrapped)) .addEquivalenceGroup(wrapped) .addEquivalenceGroup(new GifDrawableTransformation(other)) .addRegressionTest( new GifDrawableTransformation(wrapped), "a7937b64b8caa58f03721bb6bacf5c78cb235febe0e70b1b84cd99541461a08e") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameLoaderTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import android.graphics.Bitmap; import android.os.Handler; import android.os.Message; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.resource.gif.GifFrameLoader.DelayTarget; import com.bumptech.glide.load.resource.gif.GifFrameLoader.FrameCallback; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.tests.TearDownGlide; import com.bumptech.glide.tests.Util.ReturnsSelfAnswer; import com.bumptech.glide.util.Util; import java.nio.ByteBuffer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; @LooperMode(LEGACY) @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GifFrameLoaderTest { @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); @Mock private GifFrameLoader.FrameCallback callback; @Mock private GifDecoder gifDecoder; @Mock private Handler handler; @Mock private Transformation transformation; @Mock private RequestManager requestManager; private GifFrameLoader loader; private RequestBuilder requestBuilder; private Bitmap firstFrame; @SuppressWarnings("unchecked") @Before public void setUp() { MockitoAnnotations.initMocks(this); when(handler.obtainMessage(anyInt(), isA(DelayTarget.class))).thenReturn(mock(Message.class)); firstFrame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); ByteBuffer byteBuffer = ByteBuffer.allocate(10); when(gifDecoder.getData()).thenReturn(byteBuffer); requestBuilder = mock(RequestBuilder.class, new ReturnsSelfAnswer()); loader = createGifFrameLoader(handler); } @NonNull private GifFrameLoader createGifFrameLoader(Handler handler) { Glide glide = getGlideSingleton(); GifFrameLoader result = new GifFrameLoader( glide.getBitmapPool(), requestManager, gifDecoder, handler, requestBuilder, transformation, firstFrame); result.subscribe(callback); return result; } private static Glide getGlideSingleton() { return Glide.get(ApplicationProvider.getApplicationContext()); } @SuppressWarnings("unchecked") @Test public void testSetFrameTransformationSetsTransformationOnRequestBuilder() { verify(requestBuilder, times(2)).apply(isA(RequestOptions.class)); Transformation transformation = mock(Transformation.class); loader.setFrameTransformation(transformation, firstFrame); verify(requestBuilder, times(3)).apply(isA(RequestOptions.class)); } @Test(expected = NullPointerException.class) public void testSetFrameTransformationThrowsIfGivenNullTransformation() { loader.setFrameTransformation(null, null); } @Test public void testReturnsSizeFromGifDecoderAndCurrentFrame() { int decoderByteSize = 123456; when(gifDecoder.getByteSize()).thenReturn(decoderByteSize); assertThat(loader.getSize()).isEqualTo(decoderByteSize + Util.getBitmapByteSize(firstFrame)); } @Test public void testStartGetsNextFrameIfNotStartedAndWithNoLoadPending() { verify(requestBuilder).into(aTarget()); } @Test public void testGetNextFrameIncrementsSignatureAndAdvancesDecoderBeforeStartingLoad() { InOrder order = inOrder(gifDecoder, requestBuilder); order.verify(gifDecoder).advance(); order.verify(requestBuilder).apply(isA(RequestOptions.class)); order.verify(requestBuilder).into(aTarget()); } @Test public void testGetCurrentFrameReturnsFirstFrameWHenNoLoadHasCompleted() { assertThat(loader.getCurrentFrame()).isEqualTo(firstFrame); } @Test public void testGetCurrentFrameReturnsCurrentBitmapAfterLoadHasCompleted() { final Bitmap result = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); DelayTarget target = mock(DelayTarget.class); when(target.getResource()).thenReturn(result); loader.onFrameReady(target); assertEquals(result, loader.getCurrentFrame()); } @Test public void testStartDoesNotStartIfAlreadyRunning() { loader.subscribe(mock(FrameCallback.class)); verify(requestBuilder, times(1)).into(aTarget()); } @Test public void testGetNextFrameDoesNotStartLoadIfLoaderIsNotRunning() { verify(requestBuilder, times(1)).into(aTarget()); loader.unsubscribe(callback); loader.onFrameReady(mock(DelayTarget.class)); verify(requestBuilder, times(1)).into(aTarget()); } @Test public void testGetNextFrameDoesNotStartLoadIfLoadIsInProgress() { loader.unsubscribe(callback); loader.subscribe(callback); verify(requestBuilder, times(1)).into(aTarget()); } @Test public void testGetNextFrameDoesStartLoadIfRestartedAndNoLoadIsInProgress() { loader.unsubscribe(callback); loader.onFrameReady(mock(DelayTarget.class)); loader.subscribe(callback); verify(requestBuilder, times(2)).into(aTarget()); } @Test public void testGetNextFrameDoesStartLoadAfterLoadCompletesIfStarted() { loader.onFrameReady(mock(DelayTarget.class)); verify(requestBuilder, times(2)).into(aTarget()); } @Test public void testOnFrameReadyClearsPreviousFrame() { // Force the loader to create a real Handler. loader = createGifFrameLoader(null); DelayTarget previous = newDelayTarget(); Request previousRequest = mock(Request.class); previous.setRequest(previousRequest); previous.onResourceReady( Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), /* transition= */ null); DelayTarget current = mock(DelayTarget.class); when(current.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)); loader.onFrameReady(previous); loader.onFrameReady(current); verify(requestManager).clear(eq(previous)); } @Test public void testOnFrameReadyWithNullResourceDoesNotClearPreviousFrame() { // Force the loader to create a real Handler by passing null. loader = createGifFrameLoader(null); DelayTarget previous = newDelayTarget(); Request previousRequest = mock(Request.class); previous.setRequest(previousRequest); previous.onResourceReady(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), null); DelayTarget current = mock(DelayTarget.class); when(current.getResource()).thenReturn(null); loader.onFrameReady(previous); loader.onFrameReady(current); verify(previousRequest, never()).clear(); } @Test public void testDelayTargetSendsMessageWithHandlerDelayed() { long targetTime = 1234; DelayTarget delayTarget = new DelayTarget(handler, 1, targetTime); delayTarget.onResourceReady( Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), null /*glideAnimation*/ ); verify(handler).sendMessageAtTime(isA(Message.class), eq(targetTime)); } @Test public void testDelayTargetSetsResourceOnResourceReady() { DelayTarget delayTarget = new DelayTarget(handler, 1, 1); Bitmap expected = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565); delayTarget.onResourceReady(expected, null /*glideAnimation*/); assertEquals(expected, delayTarget.getResource()); } @Test public void testClearsCompletedLoadOnFrameReadyIfCleared() { // Force the loader to create a real Handler by passing null; loader = createGifFrameLoader(null); loader.clear(); DelayTarget delayTarget = newDelayTarget(); Request request = mock(Request.class); delayTarget.setRequest(request); loader.onFrameReady(delayTarget); verify(requestManager).clear(eq(delayTarget)); } @Test public void testDoesNotReturnResourceForCompletedFrameInGetCurrentFrameIfLoadCompletesWhileCleared() { loader.clear(); DelayTarget delayTarget = mock(DelayTarget.class); Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(delayTarget.getResource()).thenReturn(bitmap); loader.onFrameReady(delayTarget); assertNull(loader.getCurrentFrame()); } @Test public void onFrameReady_whenNotRunning_doesNotClearPreviouslyLoadedImage() { loader = createGifFrameLoader(/* handler= */ null); DelayTarget loaded = mock(DelayTarget.class); when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(loaded); loader.unsubscribe(callback); DelayTarget nextFrame = mock(DelayTarget.class); when(nextFrame.getResource()) .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(nextFrame); verify(requestManager, never()).clear(loaded); } @Test public void onFrameReady_whenNotRunning_clearsPendingFrameOnClear() { loader = createGifFrameLoader(/* handler= */ null); DelayTarget loaded = mock(DelayTarget.class); when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(loaded); loader.unsubscribe(callback); DelayTarget nextFrame = mock(DelayTarget.class); when(nextFrame.getResource()) .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(nextFrame); loader.clear(); verify(requestManager).clear(loaded); verify(requestManager).clear(nextFrame); } @Test public void onFrameReady_whenNotRunning_clearsOldFrameOnStart() { loader = createGifFrameLoader(/* handler= */ null); DelayTarget loaded = mock(DelayTarget.class); when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(loaded); loader.unsubscribe(callback); DelayTarget nextFrame = mock(DelayTarget.class); when(nextFrame.getResource()) .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(nextFrame); loader.subscribe(callback); verify(requestManager).clear(loaded); } @Test public void onFrameReady_whenNotRunning_callsFrameReadyWithNewFrameOnStart() { loader = createGifFrameLoader(/* handler= */ null); DelayTarget loaded = mock(DelayTarget.class); when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(loaded); loader.unsubscribe(callback); DelayTarget nextFrame = mock(DelayTarget.class); Bitmap expected = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); when(nextFrame.getResource()).thenReturn(expected); loader.onFrameReady(nextFrame); verify(callback, times(1)).onFrameReady(); loader.subscribe(callback); verify(callback, times(2)).onFrameReady(); assertThat(loader.getCurrentFrame()).isEqualTo(expected); } @Test public void onFrameReady_whenInvisible_setVisibleLater() { loader = createGifFrameLoader(/* handler= */ null); // The target is invisible at this point. loader.unsubscribe(callback); loader.setNextStartFromFirstFrame(); DelayTarget loaded = mock(DelayTarget.class); when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(loaded); loader.subscribe(callback); } @Test public void startFromFirstFrame_withPendingFrame_clearsPendingFrame() { loader = createGifFrameLoader(/* handler= */ null); DelayTarget loaded = mock(DelayTarget.class); when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); loader.onFrameReady(loaded); loader.unsubscribe(callback); DelayTarget nextFrame = mock(DelayTarget.class); Bitmap expected = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); when(nextFrame.getResource()).thenReturn(expected); loader.onFrameReady(nextFrame); loader.setNextStartFromFirstFrame(); verify(requestManager).clear(nextFrame); loader.subscribe(callback); verify(callback, times(1)).onFrameReady(); } private DelayTarget newDelayTarget() { return new DelayTarget(handler, /* index= */ 0, /* targetTime= */ 0); } @SuppressWarnings("unchecked") private static Target aTarget() { return isA(Target.class); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameResourceDecoderTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class GifFrameResourceDecoderTest { private GifDecoder gifDecoder; private GifFrameResourceDecoder resourceDecoder; private Options options; @Before public void setUp() { gifDecoder = mock(GifDecoder.class); resourceDecoder = new GifFrameResourceDecoder(mock(BitmapPool.class)); options = new Options(); } @Test public void testReturnsFrameFromGifDecoder() throws IOException { Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); when(gifDecoder.getNextFrame()).thenReturn(expected); assertEquals( expected, Preconditions.checkNotNull(resourceDecoder.decode(gifDecoder, 100, 100, options)).get()); } @Test public void testReturnsNullIfGifDecoderReturnsNullFrame() { when(gifDecoder.getNextFrame()).thenReturn(null); assertNull(resourceDecoder.decode(gifDecoder, 100, 100, options)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/gif/StreamGifDecoderTest.java ================================================ package com.bumptech.glide.load.resource.gif; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class StreamGifDecoderTest { private static final byte[] GIF_HEADER = new byte[] {0x47, 0x49, 0x46}; @Mock private ResourceDecoder byteBufferDecoder; private StreamGifDecoder decoder; private Options options; @Before public void setUp() { MockitoAnnotations.initMocks(this); List parsers = new ArrayList<>(); parsers.add(new DefaultImageHeaderParser()); decoder = new StreamGifDecoder(parsers, byteBufferDecoder, new LruArrayPool()); options = new Options(); } @Test public void testDoesNotHandleStreamIfEnabledButNotAGif() throws IOException { assertThat(decoder.handles(new ByteArrayInputStream(new byte[0]), options)).isFalse(); } @Test public void testHandlesStreamIfContainsGifHeaderAndDisabledIsNotSet() throws IOException { assertThat(decoder.handles(new ByteArrayInputStream(GIF_HEADER), options)).isTrue(); } @Test public void testHandlesStreamIfContainsGifHeaderAndDisabledIsFalse() throws IOException { options.set(GifOptions.DISABLE_ANIMATION, false); assertThat(decoder.handles(new ByteArrayInputStream(GIF_HEADER), options)).isTrue(); } @Test public void testDoesNotHandleStreamIfDisabled() throws IOException { options.set(GifOptions.DISABLE_ANIMATION, true); assertThat(decoder.handles(new ByteArrayInputStream(GIF_HEADER), options)).isFalse(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/transcode/BitmapBytesTranscoderTest.java ================================================ package com.bumptech.glide.load.resource.transcode; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.mockResource; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.util.Preconditions; import java.io.ByteArrayOutputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapBytesTranscoderTest { private BitmapBytesTranscoderHarness harness; @Before() public void setUp() { harness = new BitmapBytesTranscoderHarness(); } @Test public void testReturnsBytesOfGivenBitmap() { assertThat(harness.getTranscodeResult()).isEqualTo(harness.getExpectedData()); } @Test public void testUsesGivenQuality() { harness.quality = 66; assertThat(harness.getTranscodeResult()).isEqualTo(harness.getExpectedData()); } @Test public void testUsesGivenFormat() { for (Bitmap.CompressFormat format : Bitmap.CompressFormat.values()) { harness.compressFormat = format; assertThat(harness.getTranscodeResult()).isEqualTo(harness.getExpectedData()); } } @Test public void testBitmapResourceIsRecycled() { harness.getTranscodeResult(); verify(harness.bitmapResource).recycle(); } private static class BitmapBytesTranscoderHarness { Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.JPEG; int quality = 100; final Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8); final Resource bitmapResource = mockResource(); final Options options = new Options(); BitmapBytesTranscoderHarness() { when(bitmapResource.get()).thenReturn(bitmap); } byte[] getTranscodeResult() { BitmapBytesTranscoder transcoder = new BitmapBytesTranscoder(compressFormat, quality); Resource bytesResource = Preconditions.checkNotNull(transcoder.transcode(bitmapResource, options)); return bytesResource.get(); } byte[] getExpectedData() { ByteArrayOutputStream os = new ByteArrayOutputStream(); bitmap.compress(compressFormat, quality, os); return os.toByteArray(); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/transcode/BitmapDrawableTranscoderTest.java ================================================ package com.bumptech.glide.load.resource.transcode; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.mockResource; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapDrawableTranscoderTest { private BitmapDrawableTranscoder transcoder; @Before public void setUp() { transcoder = new BitmapDrawableTranscoder(ApplicationProvider.getApplicationContext().getResources()); } @Test public void testReturnsBitmapDrawableResourceContainingGivenBitmap() { Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Resource resource = mockResource(); when(resource.get()).thenReturn(expected); Resource transcoded = transcoder.transcode(resource, new Options()); assertEquals(expected, transcoded.get().getBitmap()); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/transcode/GifDrawableBytesTranscoderTest.java ================================================ package com.bumptech.glide.load.resource.transcode; import static com.bumptech.glide.tests.Util.mockResource; import static org.junit.Assert.assertArrayEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.resource.gif.GifDrawable; import java.nio.ByteBuffer; import java.nio.charset.Charset; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class GifDrawableBytesTranscoderTest { private GifDrawableBytesTranscoder transcoder; private GifDrawable gifDrawable; private Resource resource; @Before public void setUp() { gifDrawable = mock(GifDrawable.class); resource = mockResource(); when(resource.get()).thenReturn(gifDrawable); transcoder = new GifDrawableBytesTranscoder(); } @Test public void testReturnsBytesOfGivenGifDrawable() { for (String fakeData : new String[] {"test", "1235asfklaw3", "@$@#"}) { ByteBuffer expected = ByteBuffer.wrap(fakeData.getBytes(Charset.defaultCharset())); when(gifDrawable.getBuffer()).thenReturn(expected); Resource transcoded = transcoder.transcode(resource, new Options()); assertArrayEquals(expected.array(), transcoded.get()); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/transcode/TranscoderRegistryTest.java ================================================ package com.bumptech.glide.load.resource.transcode; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; import java.io.File; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class TranscoderRegistryTest { private TranscoderRegistry factories; @Before public void setUp() { factories = new TranscoderRegistry(); } @Test public void testReturnsUnitDecoderIfClassesAreIdentical() { assertEquals(UnitTranscoder.get(), factories.get(Object.class, Object.class)); } @Test public void testCanRegisterAndRetrieveResourceTranscoder() { @SuppressWarnings("unchecked") ResourceTranscoder transcoder = mock(ResourceTranscoder.class); factories.register(File.class, String.class, transcoder); assertEquals(transcoder, factories.get(File.class, String.class)); } @Test public void testDoesNotThrowIfRequestCanBeSatisfiedByUnitTranscoder() { // Assignable from. assertNotNull(factories.get(Integer.class, Number.class)); // Equal to. assertNotNull(factories.get(Integer.class, Integer.class)); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfNoTranscoderRegistered() { factories.get(File.class, Integer.class); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/load/resource/transcode/UnitTranscoderTest.java ================================================ package com.bumptech.glide.load.resource.transcode; import static com.bumptech.glide.tests.Util.mockResource; import static org.junit.Assert.assertEquals; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class UnitTranscoderTest { @Test public void testReturnsTheGivenResource() { Resource resource = mockResource(); ResourceTranscoder unitTranscoder = UnitTranscoder.get(); assertEquals(resource, unitTranscoder.transcode(resource, new Options())); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/manager/DefaultConnectivityMonitorFactoryTest.java ================================================ package com.bumptech.glide.manager; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.robolectric.Shadows.shadowOf; import android.app.Application; import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class DefaultConnectivityMonitorFactoryTest { private ConnectivityMonitorFactory factory; @Before public void setUp() { factory = new DefaultConnectivityMonitorFactory(); } @Test public void testReturnsDefaultConnectivityMonitorWhenHasPermission() { shadowOf((Application) ApplicationProvider.getApplicationContext()) .grantPermissions("android.permission.ACCESS_NETWORK_STATE"); ConnectivityMonitor connectivityMonitor = factory.build( ApplicationProvider.getApplicationContext(), mock(ConnectivityMonitor.ConnectivityListener.class)); assertThat(connectivityMonitor).isInstanceOf(DefaultConnectivityMonitor.class); } @Test public void testReturnsNullConnectivityMonitorWhenDoesNotHavePermission() { ConnectivityMonitor connectivityMonitor = factory.build( ApplicationProvider.getApplicationContext(), mock(ConnectivityMonitor.ConnectivityListener.class)); assertThat(connectivityMonitor).isInstanceOf(NullConnectivityMonitor.class); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/manager/DefaultConnectivityMonitorTest.java ================================================ package com.bumptech.glide.manager; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import android.app.Application; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkInfo; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.manager.DefaultConnectivityMonitorTest.PermissionConnectivityManager; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.LooperMode; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowConnectivityManager; import org.robolectric.shadows.ShadowNetwork; import org.robolectric.shadows.ShadowNetworkInfo; @LooperMode(LEGACY) @RunWith(RobolectricTestRunner.class) @Config( sdk = {24}, shadows = PermissionConnectivityManager.class) public class DefaultConnectivityMonitorTest { @Mock private ConnectivityMonitor.ConnectivityListener listener; private DefaultConnectivityMonitor monitor; private ConnectivityHarness harness; @Before public void setUp() { MockitoAnnotations.initMocks(this); monitor = new DefaultConnectivityMonitor(ApplicationProvider.getApplicationContext(), listener); harness = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new ConnectivityHarnessPost24() : new ConnectivityHarnessPre24(); } @After public void tearDown() { SingletonConnectivityReceiver.reset(); } @Test public void testRegistersReceiverOnStart() { monitor.onStart(); assertThat(harness.getRegisteredReceivers()).isEqualTo(1); } @Test public void testDoesNotRegisterTwiceOnStart() { monitor.onStart(); monitor.onStart(); assertThat(harness.getRegisteredReceivers()).isEqualTo(1); } @Test public void testUnregistersReceiverOnStop() { monitor.onStart(); monitor.onStop(); assertThat(harness.getRegisteredReceivers()).isEqualTo(0); } @Test public void testHandlesUnregisteringTwiceInARow() { monitor.onStop(); monitor.onStop(); assertThat(harness.getRegisteredReceivers()).isEqualTo(0); } @Test public void testDoesNotNotifyListenerIfConnectedAndBecomesConnected() { harness.connect(); monitor.onStart(); harness.broadcast(); verify(listener, never()).onConnectivityChanged(anyBoolean()); } @Test public void testNotifiesListenerIfConnectedAndBecomesDisconnected() { harness.connect(); monitor.onStart(); harness.disconnect(); harness.broadcast(); verify(listener).onConnectivityChanged(eq(false)); } @Test public void testNotifiesListenerIfDisconnectedAndBecomesConnected() { harness.disconnect(); monitor.onStart(); harness.connect(); harness.broadcast(); verify(listener).onConnectivityChanged(true); } @Test public void testDoesNotNotifyListenerWhenNotRegistered() { harness.disconnect(); monitor.onStart(); monitor.onStop(); harness.connect(); harness.broadcast(); verify(listener, never()).onConnectivityChanged(anyBoolean()); } @Test public void register_withMissingPermission_doesNotThrow() { harness.setNetworkPermissionGranted(false); monitor.onStart(); } @Test public void onReceive_withMissingPermission_doesNotThrow() { monitor.onStart(); harness.setNetworkPermissionGranted(false); harness.broadcast(); } @Test public void onReceive_withMissingPermission_previouslyDisconnected_notifiesListenersConnected() { harness.disconnect(); monitor.onStart(); harness.setNetworkPermissionGranted(false); harness.broadcast(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { verify(listener).onConnectivityChanged(true); } else { verify(listener, never()).onConnectivityChanged(anyBoolean()); } } @Test public void onReceive_withMissingPermission_previouslyConnected_doesNotNotifyListeners() { harness.connect(); monitor.onStart(); harness.setNetworkPermissionGranted(false); harness.broadcast(); verify(listener, never()).onConnectivityChanged(anyBoolean()); } private interface ConnectivityHarness { void connect(); void disconnect(); void broadcast(); void setNetworkPermissionGranted(boolean isGranted); int getRegisteredReceivers(); } private static final class ConnectivityHarnessPost24 implements ConnectivityHarness { private final PermissionConnectivityManager shadowConnectivityManager; ConnectivityHarnessPost24() { ConnectivityManager connectivityManager = (ConnectivityManager) ApplicationProvider.getApplicationContext() .getSystemService(Context.CONNECTIVITY_SERVICE); shadowConnectivityManager = Shadow.extract(connectivityManager); } @Override public void connect() { shadowConnectivityManager.isConnected = true; } @Override public void disconnect() { shadowConnectivityManager.isConnected = false; } @Override public void broadcast() { for (NetworkCallback callback : shadowConnectivityManager.getNetworkCallbacks()) { if (shadowConnectivityManager.isConnected) { callback.onAvailable(null); } else { callback.onLost(null); } } } @Override public void setNetworkPermissionGranted(boolean isGranted) { shadowConnectivityManager.isNetworkPermissionGranted = isGranted; } @Override public int getRegisteredReceivers() { return shadowConnectivityManager.getNetworkCallbacks().size(); } } private static final class ConnectivityHarnessPre24 implements ConnectivityHarness { private final PermissionConnectivityManager shadowConnectivityManager; public ConnectivityHarnessPre24() { ConnectivityManager connectivityManager = (ConnectivityManager) ApplicationProvider.getApplicationContext() .getSystemService(Context.CONNECTIVITY_SERVICE); shadowConnectivityManager = Shadow.extract(connectivityManager); } @Override public void disconnect() { shadowConnectivityManager.setActiveNetworkInfo(null); } @Override public void connect() { NetworkInfo networkInfo = ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, 0, 0, true, true); shadowConnectivityManager.setActiveNetworkInfo(networkInfo); } @Override public void broadcast() { Intent connected = new Intent(ConnectivityManager.CONNECTIVITY_ACTION); ApplicationProvider.getApplicationContext().sendBroadcast(connected); } @Override public void setNetworkPermissionGranted(boolean isGranted) { shadowConnectivityManager.isNetworkPermissionGranted = isGranted; } @Override public int getRegisteredReceivers() { Intent connectivity = new Intent(ConnectivityManager.CONNECTIVITY_ACTION); return shadowOf((Application) ApplicationProvider.getApplicationContext()) .getReceiversForIntent(connectivity) .size(); } } @Implements(ConnectivityManager.class) public static final class PermissionConnectivityManager extends ShadowConnectivityManager { private boolean isNetworkPermissionGranted = true; private boolean isConnected; @Implementation @Override public Network getActiveNetwork() { if (isConnected) { return ShadowNetwork.newInstance(1); } else { return null; } } @Implementation @Override protected void registerDefaultNetworkCallback(NetworkCallback networkCallback) { if (!isNetworkPermissionGranted) { throw new SecurityException(); } super.registerDefaultNetworkCallback(networkCallback); if (isConnected) { networkCallback.onAvailable(null); } else { networkCallback.onLost(null); } } @Implementation @Override public NetworkInfo getActiveNetworkInfo() { if (!isNetworkPermissionGranted) { throw new SecurityException(); } return super.getActiveNetworkInfo(); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/manager/Issue117Activity.java ================================================ package com.bumptech.glide.manager; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import com.bumptech.glide.Glide; /** A test activity to reproduce Issue #117: https://github.com/bumptech/glide/issues/117. */ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) class Issue117Activity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewPager viewPager = new ViewPager(this); viewPager.setId(View.generateViewId()); setContentView(viewPager, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); viewPager.setAdapter(new Issue117Adapter(getSupportFragmentManager())); } private static class Issue117Adapter extends FragmentPagerAdapter { Issue117Adapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { return new Issue117Fragment(); } @Override public int getCount() { return 1; } } public static class Issue117Fragment extends Fragment { @Override public View onCreateView( @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return new Issue117ImageView(getActivity()); } } public static class Issue117ImageView extends ImageView { public Issue117ImageView(Context context) { super(context); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); Glide.with(getContext()).asDrawable().load(android.R.drawable.ic_menu_rotate).into(this); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/manager/RequestManagerRetrieverTest.java ================================================ package com.bumptech.glide.manager; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.BackgroundUtil.testInBackground; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.LayoutInflater; import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentController; import androidx.fragment.app.FragmentHostCallback; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.tests.BackgroundUtil.BackgroundTester; import com.bumptech.glide.tests.TearDownGlide; import com.bumptech.glide.tests.Util; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowLooper; @LooperMode(LEGACY) @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class RequestManagerRetrieverTest { @Rule public TearDownGlide tearDownGlide = new TearDownGlide(); private static final String PARENT_TAG = "parent"; private Context appContext; private int initialSdkVersion; private RequestManagerRetriever retriever; @Before public void setUp() { appContext = ApplicationProvider.getApplicationContext(); retriever = new RequestManagerRetriever(/* factory= */ null); initialSdkVersion = Build.VERSION.SDK_INT; Util.setSdkVersionInt(18); } @After public void tearDown() { Util.setSdkVersionInt(initialSdkVersion); Shadows.shadowOf(Looper.getMainLooper()).runToEndOfTasks(); } @Test public void testHasValidTag() { assertEquals( RequestManagerRetriever.class.getPackage().getName(), RequestManagerRetriever.FRAGMENT_TAG); } @Test public void testCanGetRequestManagerFromActivity() { Activity activity = Robolectric.buildActivity(Activity.class).create().start().get(); RequestManager manager = retriever.get(activity); assertEquals(manager, retriever.get(activity)); } @Test public void testSupportCanGetRequestManagerFromActivity() { FragmentActivity fragmentActivity = Robolectric.buildActivity(FragmentActivity.class).create().start().get(); RequestManager manager = retriever.get(fragmentActivity); assertEquals(manager, retriever.get(fragmentActivity)); } @SuppressWarnings("deprecation") @Test public void testCanGetRequestManagerFromFragment() { Activity activity = Robolectric.buildActivity(Activity.class).create().start().resume().get(); android.app.Fragment fragment = new android.app.Fragment(); activity.getFragmentManager().beginTransaction().add(fragment, PARENT_TAG).commit(); activity.getFragmentManager().executePendingTransactions(); RequestManager manager = retriever.get(fragment); assertEquals(manager, retriever.get(fragment)); } @Test public void testSupportCanGetRequestManagerFromFragment() { FragmentActivity activity = Robolectric.buildActivity(FragmentActivity.class).create().start().resume().get(); Fragment fragment = new Fragment(); activity.getSupportFragmentManager().beginTransaction().add(fragment, PARENT_TAG).commit(); activity.getSupportFragmentManager().executePendingTransactions(); RequestManager manager = retriever.get(fragment); assertEquals(manager, retriever.get(fragment)); } @Test public void testSupportCanGetRequestManagerFromFragment_nonActivityController() { FragmentController controller = FragmentController.createController(new NonActivityHostCallback(appContext)); controller.attachHost(/* fragment= */ null); controller.dispatchCreate(); controller.dispatchStart(); controller.dispatchResume(); Fragment fragment = new Fragment(); controller.getSupportFragmentManager().beginTransaction().add(fragment, PARENT_TAG).commit(); controller.getSupportFragmentManager().executePendingTransactions(); RequestManager manager = retriever.get(fragment); assertEquals(manager, retriever.get(fragment)); } @Test public void testCanGetRequestManagerFromDetachedFragment() { helpTestCanGetRequestManagerFromDetachedFragment(); } @Test public void testCanGetRequestManagerFromDetachedFragment_PreJellyBeanMr1() { Util.setSdkVersionInt(Build.VERSION_CODES.JELLY_BEAN); helpTestCanGetRequestManagerFromDetachedFragment(); } @SuppressWarnings("deprecation") private void helpTestCanGetRequestManagerFromDetachedFragment() { Activity activity = Robolectric.buildActivity(Activity.class).create().start().resume().get(); android.app.Fragment fragment = new android.app.Fragment(); activity .getFragmentManager() .beginTransaction() .add(fragment, PARENT_TAG) .detach(fragment) .commit(); activity.getFragmentManager().executePendingTransactions(); assertTrue(fragment.isDetached()); retriever.get(fragment); } @Test public void testSupportCanGetRequestManagerFromDetachedFragment() { helpTestSupportCanGetRequestManagerFromDetachedFragment(); } @Test public void testSupportCanGetRequestManagerFromDetachedFragment_PreJellyBeanMr1() { Util.setSdkVersionInt(Build.VERSION_CODES.JELLY_BEAN); helpTestSupportCanGetRequestManagerFromDetachedFragment(); } private void helpTestSupportCanGetRequestManagerFromDetachedFragment() { FragmentActivity activity = Robolectric.buildActivity(FragmentActivity.class).create().start().resume().get(); Fragment fragment = new Fragment(); activity .getSupportFragmentManager() .beginTransaction() .add(fragment, PARENT_TAG) .detach(fragment) .commit(); activity.getSupportFragmentManager().executePendingTransactions(); assertTrue(fragment.isDetached()); retriever.get(fragment); } @SuppressWarnings("deprecation") @Test(expected = IllegalArgumentException.class) public void testThrowsIfFragmentNotAttached() { android.app.Fragment fragment = new android.app.Fragment(); retriever.get(fragment); } @Test(expected = NullPointerException.class) public void testThrowsIfSupportFragmentNotAttached() { Fragment fragment = new Fragment(); retriever.get(fragment); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfGivenNullContext() { retriever.get((Context) null); } @Test public void testHandlesContextWrappersForApplication() { ContextWrapper contextWrapper = new ContextWrapper(appContext); RequestManager requestManager = retriever.get(appContext); assertEquals(requestManager, retriever.get(contextWrapper)); } @Test public void testHandlesContextWrapperWithoutApplication() throws Exception { // Create a Context which is not associated with an Application instance. Context baseContext = appContext.createPackageContext(appContext.getPackageName(), /* flags= */ 0); // Sanity-check that Robolectric behaves the same as the framework. assertThat(baseContext.getApplicationContext()).isNull(); // If a wrapper provides a non-null application Context, unwrapping should terminate at this // wrapper so that the returned Context has a non-null #getApplicationContext. Context contextWithApplicationContext = new ContextWrapper(baseContext) { @Override public Context getApplicationContext() { return this; } }; Context wrappedContext = new ContextWrapper(contextWithApplicationContext); RequestManager requestManager = retriever.get(appContext); assertEquals(requestManager, retriever.get(wrappedContext)); } @Test public void testReturnsNonNullManagerIfGivenApplicationContext() { assertNotNull(retriever.get(appContext)); } @Test public void testApplicationRequestManagerIsNotPausedWhenRetrieved() { RequestManager manager = retriever.get(appContext); assertFalse(manager.isPaused()); } @Test public void testApplicationRequestManagerIsNotReResumedAfterFirstRetrieval() { RequestManager manager = retriever.get(appContext); manager.pauseRequests(); manager = retriever.get(appContext); assertTrue(manager.isPaused()); } @Test public void testDoesNotThrowWhenGetWithContextCalledFromBackgroundThread() throws InterruptedException { testInBackground( new BackgroundTester() { @Override public void runTest() { retriever.get(appContext); } }); } // See Issue #117: https://github.com/bumptech/glide/issues/117. @Test public void testCanCallGetInOnAttachToWindowInFragmentInViewPager() { // Robolectric by default runs messages posted to the main looper synchronously, the // framework does not. We post // to the main thread here to work around an issue caused by a recursive method call so we // need (and reasonably // expect) our message to not run immediately Shadows.shadowOf(Looper.getMainLooper()).pause(); Robolectric.buildActivity(Issue117Activity.class).create().start().resume().visible(); } @Test @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public void testDoesNotThrowIfAskedToGetManagerForActivityPreJellYBeanMr1() { Util.setSdkVersionInt(Build.VERSION_CODES.JELLY_BEAN); Activity activity = Robolectric.buildActivity(Activity.class).create().start().resume().get(); Activity spyActivity = Mockito.spy(activity); when(spyActivity.isDestroyed()).thenThrow(new NoSuchMethodError()); assertNotNull(retriever.get(spyActivity)); } @SuppressWarnings("deprecation") @Test @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public void testDoesNotThrowIfAskedToGetManagerForFragmentPreJellyBeanMr1() { Util.setSdkVersionInt(Build.VERSION_CODES.JELLY_BEAN); Activity activity = Robolectric.buildActivity(Activity.class).create().start().resume().get(); android.app.Fragment fragment = new android.app.Fragment(); activity.getFragmentManager().beginTransaction().add(fragment, "test").commit(); android.app.Fragment spyFragment = Mockito.spy(fragment); when(spyFragment.getChildFragmentManager()).thenThrow(new NoSuchMethodError()); assertNotNull(retriever.get(spyFragment)); } @Test public void get_beforeActivityIsCreated_returnsSameRequestManagerAsAfterActivityIsCreated() { ShadowLooper shadowLooper = Shadows.shadowOf(Looper.getMainLooper()); shadowLooper.pause(); ActivityController controller = Robolectric.buildActivity(FragmentActivity.class); RequestManager beforeCreateRequestManager = Glide.with(controller.get()); // Make sure that the activity makes it one frame without being created. controller.create().start(); // Simulate finishing at least one frame before the next attempt to get a RequestManager shadowLooper.runOneTask(); // Try to get the request manager again. If we've successfully retained the Fragment we wanted // to add, then we should get the same instance. If we added a new Fragment instance, the // RequestManager won't match and things will be broken. RequestManager afterCreateRequestManager = Glide.with(controller.get()); assertThat(afterCreateRequestManager).isEqualTo(beforeCreateRequestManager); } @Test public void get_onDetachedFragment_returnsSameRequestManagerAsAfterFragmentIsAttached() { ShadowLooper shadowLooper = Shadows.shadowOf(Looper.getMainLooper()); shadowLooper.pause(); ActivityController controller = Robolectric.buildActivity(FragmentActivity.class); controller.create(); FragmentActivity fragmentActivity = controller.get(); Fragment childFragment = new Fragment(); fragmentActivity .getSupportFragmentManager() .beginTransaction() .add(childFragment, "TEST_TAG") .commitNow(); fragmentActivity .getSupportFragmentManager() .beginTransaction() .detach(childFragment) .commitNow(); RequestManager beforeAttachRequestManager = Glide.with(childFragment); shadowLooper.runOneTask(); fragmentActivity .getSupportFragmentManager() .beginTransaction() .attach(childFragment) .commitNow(); RequestManager afterAttachRequestManager = Glide.with(childFragment); assertThat(afterAttachRequestManager).isEqualTo(beforeAttachRequestManager); } /** Simple callback for creating an Activity-less Fragment host. */ private final class NonActivityHostCallback extends FragmentHostCallback { private final Context context; NonActivityHostCallback(Context context) { super(context, new Handler(Looper.getMainLooper()), /* windowAnimations= */ 0); this.context = context; } @Override public LayoutInflater onGetLayoutInflater() { return LayoutInflater.from(context).cloneInContext(context); } @Override public RequestManagerRetrieverTest onGetHost() { return RequestManagerRetrieverTest.this; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/manager/RequestTrackerTest.java ================================================ package com.bumptech.glide.manager; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.bumptech.glide.request.Request; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class RequestTrackerTest { private RequestTracker tracker; @Before public void setUp() { MockitoAnnotations.initMocks(this); tracker = new RequestTracker(); } @Test public void clearAndRemove_withRequestPreviouslyClearedInClearRequests_doesNothing() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); tracker.clearRequests(); tracker.clearAndRemove(request); assertThat(request.isCleared()).isTrue(); } @Test public void clearAndRemove_withNullRequest_doesNothingAndReturnsTrue() { assertThat(tracker.clearAndRemove(null)).isTrue(); } @Test public void clearAndRemove_withUnTrackedRequest_doesNothingAndReturnsFalse() { FakeRequest request = new FakeRequest(); assertThat(tracker.clearAndRemove(request)).isFalse(); assertThat(request.isCleared()).isFalse(); } @Test public void clearAndRemov_withTrackedRequest_clearssAndReturnsTrue() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); assertThat(tracker.clearAndRemove(request)).isTrue(); assertThat(request.isCleared()).isTrue(); } @Test public void clearAndRemove_withAlreadyRemovedRequest_doesNothingAndReturnsFalse() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); tracker.clearAndRemove(request); assertThat(tracker.clearAndRemove(request)).isFalse(); assertThat(request.isCleared()).isTrue(); } @Test public void clearRequests_withPreviouslyClearedRequest_doesNotClearRequestAgain() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); tracker.clearAndRemove(request); tracker.clearRequests(); assertThat(request.isCleared()).isTrue(); } @Test public void clearRequests_withMultipleRequests_clearsAllRequests() { FakeRequest first = new FakeRequest(); FakeRequest second = new FakeRequest(); tracker.addRequest(first); tracker.addRequest(second); tracker.clearRequests(); assertThat(first.isCleared()).isTrue(); assertThat(second.isCleared()).isTrue(); } @Test public void pauseRequest_withRunningRequest_pausesRequest() { FakeRequest request = new FakeRequest(); request.setIsRunning(); tracker.addRequest(request); tracker.pauseRequests(); assertThat(request.isPaused()).isTrue(); } @Test public void pauseRequests_withCompletedRequest_doesNotPauseRequest() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); request.setIsComplete(); tracker.pauseRequests(); assertThat(request.isPaused()).isFalse(); } @Test public void pauseRequests_withClearedRequest_doesNotPauseRequest() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); request.clear(); tracker.pauseRequests(); assertThat(request.isPaused()).isFalse(); } @Test public void runRequest_startsRequest() { FakeRequest request = new FakeRequest(); tracker.runRequest(request); assertThat(request.isRunning()).isTrue(); } @Test public void runRequest_whenPaused_doesNotStartRequest() { FakeRequest request = new FakeRequest(); tracker.pauseRequests(); tracker.runRequest(request); assertThat(request.isRunning()).isFalse(); } @Test public void runRequest_withAllRequestsPaused_doesNotStartRequest() { FakeRequest request = new FakeRequest(); tracker.pauseAllRequests(); tracker.runRequest(request); assertThat(request.isRunning()).isFalse(); } @Test public void runRequest_afterPausingAndResuming_startsRequest() { FakeRequest request = new FakeRequest(); tracker.pauseRequests(); tracker.runRequest(request); tracker.resumeRequests(); assertThat(request.isRunning()).isTrue(); } @Test public void resumeRequests_withRequestAddedWhilePaused_startsRequest() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); tracker.resumeRequests(); assertThat(request.isRunning()).isTrue(); } @Test public void resumeRequests_withCompletedRequest_doesNotRestartCompletedRequest() { FakeRequest request = new FakeRequest(); request.setIsComplete(); tracker.addRequest(request); tracker.resumeRequests(); assertThat(request.isRunning()).isFalse(); } @Test public void addRequest_withRunningRequest_doesNotRestartRequest() { FakeRequest request = new FakeRequest(); request.setIsRunning(); tracker.addRequest(request); tracker.resumeRequests(); assertThat(request.isRunning()).isTrue(); } @Test public void resumeRequests_withRequestThatClearsAnotherRequest_avoidsConcurrentModifications() { Request first = mock(Request.class); Request second = mock(Request.class); doAnswer(new ClearAndRemoveRequest(second)).when(first).begin(); tracker.addRequest(mock(Request.class)); tracker.addRequest(first); tracker.addRequest(second); tracker.resumeRequests(); } @Test public void pauseRequests_withRequestThatClearsAnother_avoidsConcurrentModifications() { Request first = mock(Request.class); Request second = mock(Request.class); when(first.isRunning()).thenReturn(true); doAnswer(new ClearAndRemoveRequest(second)).when(first).clear(); tracker.addRequest(mock(Request.class)); tracker.addRequest(first); tracker.addRequest(second); tracker.pauseRequests(); } @Test public void clearRequests_withRequestThatClearsAnother_avoidsConcurrentModifications() { Request first = mock(Request.class); Request second = mock(Request.class); doAnswer(new ClearAndRemoveRequest(second)).when(first).clear(); tracker.addRequest(mock(Request.class)); tracker.addRequest(first); tracker.addRequest(second); tracker.clearRequests(); } @Test public void restartRequests_withRequestThatClearsAnother_avoidsConcurrentModifications() { Request first = mock(Request.class); Request second = mock(Request.class); doAnswer(new ClearAndRemoveRequest(second)).when(first).clear(); tracker.addRequest(mock(Request.class)); tracker.addRequest(first); tracker.addRequest(second); tracker.restartRequests(); } @Test public void restartRequests_withIncompleteRequest_restartsRequest() { FakeRequest request = new FakeRequest(); tracker.addRequest(request); tracker.restartRequests(); assertThat(request.isRunning()).isTrue(); } @Test public void restartRequests_whenPaused_doesNotRestartRequests() { FakeRequest request = new FakeRequest(); request.setIsComplete(); tracker.pauseRequests(); tracker.addRequest(request); tracker.restartRequests(); assertThat(request.isRunning()).isFalse(); } @Test public void restartRequests_withIncompleteRequestAddedWhilePaused_doesNotRestartRequest() { FakeRequest request = new FakeRequest(); tracker.pauseRequests(); tracker.addRequest(request); tracker.restartRequests(); assertThat(request.isRunning()).isFalse(); } @Test public void restartRequests_withIncompleteRequestAddedWhilePaused_clearsRequestOnRestart() { FakeRequest request = new FakeRequest(); tracker.pauseRequests(); tracker.addRequest(request); tracker.restartRequests(); assertThat(request.isCleared()).isTrue(); } @Test public void testReturnsTrueFromIsPausedWhenPaused() { tracker.pauseRequests(); assertTrue(tracker.isPaused()); } @Test public void pauseRequests_pausesRunningRequest() { FakeRequest request = new FakeRequest(); request.setIsRunning(); tracker.addRequest(request); tracker.pauseRequests(); assertThat(request.isCleared()).isTrue(); } @Test public void pauseRequest_doesNotPauseCompletedRequest() { FakeRequest request = new FakeRequest(); request.setIsComplete(); tracker.addRequest(request); tracker.pauseRequests(); assertThat(request.isComplete()).isTrue(); assertThat(request.isCleared()).isFalse(); } @Test public void testReturnsFalseFromIsPausedWhenResumed() { tracker.resumeRequests(); assertFalse(tracker.isPaused()); } @Test public void testPauseAllRequests_returnsTrueFromIsPaused() { tracker.pauseAllRequests(); assertTrue(tracker.isPaused()); } @Test public void resumeRequests_afterRequestIsPausedViaPauseAllRequests_resumesRequest() { FakeRequest request = new FakeRequest(); request.setIsComplete(); tracker.addRequest(request); tracker.pauseAllRequests(); assertThat(request.isCleared()).isTrue(); // reset complete status. request.setIsComplete(false); tracker.resumeRequests(); assertThat(request.isRunning()).isTrue(); } private static final class FakeRequest implements Request { private boolean isPaused; @Override public void pause() { isPaused = true; if (isRunning) { clear(); } } private boolean isRunning; private boolean isCleared; private boolean isComplete; void setIsComplete() { setIsComplete(true); } void setIsComplete(boolean isComplete) { this.isComplete = isComplete; } void setIsRunning() { isRunning = true; } boolean isPaused() { return isPaused; } @Override public void begin() { if (isRunning) { throw new IllegalStateException(); } isRunning = true; } @Override public void clear() { if (isCleared) { throw new IllegalStateException(); } isRunning = false; isCleared = true; } @Override public boolean isRunning() { return isRunning; } @Override public boolean isComplete() { return isComplete; } @Override public boolean isCleared() { return isCleared; } @Override public boolean isAnyResourceSet() { return isComplete; } @Override public boolean isEquivalentTo(Request other) { throw new UnsupportedOperationException(); } } private class ClearAndRemoveRequest implements Answer { private final Request toRemove; ClearAndRemoveRequest(Request toRemove) { this.toRemove = toRemove; } @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { tracker.clearAndRemove(toRemove); return null; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/module/ManifestParserTest.java ================================================ package com.bumptech.glide.module; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) @SuppressWarnings("deprecation") public class ManifestParserTest { private static final String MODULE_VALUE = "GlideModule"; private static final String PACKAGE_NAME = "com.bumptech.test"; @Mock private Context context; private ManifestParser parser; private ApplicationInfo applicationInfo; @Before public void setUp() throws PackageManager.NameNotFoundException { MockitoAnnotations.initMocks(this); applicationInfo = new ApplicationInfo(); applicationInfo.metaData = new Bundle(); when(context.getPackageName()).thenReturn(PACKAGE_NAME); PackageManager pm = mock(PackageManager.class); when(pm.getApplicationInfo(eq(PACKAGE_NAME), eq(PackageManager.GET_META_DATA))) .thenReturn(applicationInfo); when(context.getPackageManager()).thenReturn(pm); parser = new ManifestParser(context); } // TODO(#4977): Remove this after the bug in Compose's previews is fixed. @Test public void parse_withNullApplicationInfo_doesNotThrow() throws NameNotFoundException { PackageManager pm = mock(PackageManager.class); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(null); when(context.getPackageManager()).thenReturn(pm); parser = new ManifestParser(context); parser.parse(); } @Test public void testParse_returnsEmptyListIfNoModulesListed() { assertThat(parser.parse()).isEmpty(); } @Test public void testParse_withSingleValidModuleName_returnsListContainingModule() { addModuleToManifest(TestModule1.class); List modules = parser.parse(); assertThat(modules).hasSize(1); assertThat(modules.get(0)).isInstanceOf(TestModule1.class); } @Test public void testParse_withMultipleValidModuleNames_returnsListContainingModules() { addModuleToManifest(TestModule1.class); addModuleToManifest(TestModule2.class); List modules = parser.parse(); assertThat(modules).hasSize(2); assertThat(modules).contains(new TestModule1()); assertThat(modules).contains(new TestModule2()); } @Test public void testParse_withValidModuleName_ignoresMetadataWithoutGlideModuleValue() { applicationInfo.metaData.putString(TestModule1.class.getName(), MODULE_VALUE + "test"); assertThat(parser.parse()).isEmpty(); } @Test(expected = RuntimeException.class) public void testThrows_whenModuleNameNotFound() { addToManifest("fakeClassName"); parser.parse(); } @Test(expected = RuntimeException.class) public void testThrows_whenClassInManifestIsNotAModule() { addModuleToManifest(InvalidClass.class); parser.parse(); } @Test public void parse_withNullMetadata_doesNotThrow() throws NameNotFoundException { PackageManager pm = mock(PackageManager.class); ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.metaData = null; when(pm.getApplicationInfo(eq(PACKAGE_NAME), eq(PackageManager.GET_META_DATA))) .thenReturn(applicationInfo); when(context.getPackageManager()).thenReturn(pm); parser.parse(); } @Test public void parse_withMissingName_doesNotThrow() throws NameNotFoundException { PackageManager pm = mock(PackageManager.class); doThrow(new NameNotFoundException("name")).when(pm).getApplicationInfo(anyString(), anyInt()); when(context.getPackageManager()).thenReturn(pm); parser.parse(); } private void addModuleToManifest(Class moduleClass) { addToManifest(moduleClass.getName()); } private void addToManifest(String key) { applicationInfo.metaData.putString(key, MODULE_VALUE); } private static class InvalidClass {} public static class TestModule1 implements GlideModule { @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {} @Override public void registerComponents(Context context, Glide glide, Registry registry) {} @Override public boolean equals(Object o) { return o instanceof TestModule1; } @Override public int hashCode() { return super.hashCode(); } } public static class TestModule2 implements GlideModule { @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {} @Override public void registerComponents(Context context, Glide glide, Registry registry) {} @Override public boolean equals(Object o) { return o instanceof TestModule2; } @Override public int hashCode() { return super.hashCode(); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/ErrorRequestCoordinatorTest.java ================================================ package com.bumptech.glide.request; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.annotation.Nullable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(JUnit4.class) public class ErrorRequestCoordinatorTest { @Mock private Request primary; @Mock private Request error; @Mock private RequestCoordinator parent; private ErrorRequestCoordinator coordinator; @Before public void setUp() { MockitoAnnotations.initMocks(this); coordinator = newCoordinator(); coordinator.setRequests(primary, error); } @Test public void begin_startsPrimary() { coordinator.begin(); verify(primary).begin(); } @Test public void begin_whenPrimaryIsAlreadyRunning_doesNotStartPrimaryAgain() { coordinator.begin(); coordinator.begin(); verify(primary, times(1)).begin(); } @Test public void clear_whenPrimaryHasNotFailed_clearsPrimary() { coordinator.clear(); verify(primary).clear(); } @Test public void clear_whenPrimaryHasNotFailed_doesNotClearError() { coordinator.clear(); verify(error, never()).clear(); } @Test public void clear_whenPrimaryHasFailed_errorIsRunning_clearsError() { coordinator.onRequestFailed(primary); coordinator.clear(); verify(error).clear(); } @Test public void clear_whenPrimaryHasFailed_clearsPrimary() { coordinator.onRequestFailed(primary); coordinator.clear(); verify(primary).clear(); } @Test public void clear_whenErrorIsRunning_clearsError() { coordinator.onRequestFailed(primary); coordinator.clear(); verify(error).clear(); } @Test public void pause_whenPrimaryIsRunning_pausesPrimary() { coordinator.begin(); coordinator.pause(); verify(primary).pause(); } @Test public void pause_whenPrimaryIsComplete_doesNotPausePrimary() { coordinator.onRequestSuccess(primary); coordinator.pause(); verify(primary, never()).pause(); } @Test public void pause_whenPrimaryIsFailed_doesNotPausePrimary() { coordinator.onRequestFailed(primary); coordinator.pause(); verify(primary, never()).pause(); } @Test public void pause_whenErrorIsNotRunning_doesNotPauseError() { coordinator.pause(); verify(error, never()).pause(); } @Test public void pause_whenErrorIsComplete_doesNotPauseError() { coordinator.onRequestSuccess(error); coordinator.pause(); verify(error, never()).pause(); } @Test public void pause_whenErrorIsFailed_doesNotPauseError() { coordinator.onRequestFailed(error); coordinator.pause(); verify(error, never()).pause(); } @Test public void pause_whenErrorIsRunning_pausesError() { coordinator.onRequestFailed(primary); coordinator.pause(); verify(error).pause(); } @Test public void isRunning_primaryNotFailed_primaryNotRunning_returnsFalse() { assertThat(coordinator.isRunning()).isFalse(); } @Test public void isRunning_primaryNotFailed_primaryRunning_returnsTrue() { coordinator.begin(); assertThat(coordinator.isRunning()).isTrue(); } @Test public void isRunning_primaryFailed_returnsTrue() { coordinator.onRequestFailed(primary); // A failed primary request starts the error request. assertThat(coordinator.isRunning()).isTrue(); } @Test public void isComplete_primaryNotFailed_primaryNotComplete_returnsFalse() { assertThat(coordinator.isComplete()).isFalse(); } @Test public void isComplete_primaryNotFailed_primaryComplete_returnsTrue() { coordinator.onRequestSuccess(primary); assertThat(coordinator.isComplete()).isTrue(); } @Test public void isComplete_primaryFailed_errorNotComplete_returnsFalse() { coordinator.onRequestFailed(primary); assertThat(coordinator.isComplete()).isFalse(); } @Test public void isComplete_primaryFailed_errorComplete_returnsTrue() { coordinator.onRequestFailed(primary); coordinator.onRequestSuccess(error); assertThat(coordinator.isComplete()).isTrue(); } @Test public void isCleared_primaryNotFailed_primaryNotCancelled_returnsFalse() { coordinator.begin(); assertThat(coordinator.isCleared()).isFalse(); } @Test public void isCleared_primaryNotFailed_primaryCancelled_returnsTrue() { coordinator.begin(); coordinator.clear(); assertThat(coordinator.isCleared()).isTrue(); } @Test public void isCleared_primaryFailed_errorNotCancelled_returnsFalse() { coordinator.onRequestFailed(primary); assertThat(coordinator.isCleared()).isFalse(); } @Test public void isCleared_primaryFailed_errorCancelled_returnsTrue() { coordinator.onRequestFailed(primary); coordinator.clear(); assertThat(coordinator.isCleared()).isTrue(); } @Test public void isEquivalentTo() { assertThat(coordinator.isEquivalentTo(primary)).isFalse(); ErrorRequestCoordinator other = newCoordinator(/* parent= */ null); assertThat(coordinator.isEquivalentTo(other)).isFalse(); other.setRequests(primary, primary); assertThat(coordinator.isEquivalentTo(other)).isFalse(); other.setRequests(error, error); assertThat(coordinator.isEquivalentTo(other)).isFalse(); when(primary.isEquivalentTo(primary)).thenReturn(true); when(error.isEquivalentTo(error)).thenReturn(true); other.setRequests(primary, error); assertThat(coordinator.isEquivalentTo(other)).isTrue(); other = newCoordinator(parent); other.setRequests(primary, error); assertThat(coordinator.isEquivalentTo(other)).isTrue(); coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); other = newCoordinator(parent); other.setRequests(primary, error); assertThat(coordinator.isEquivalentTo(other)).isTrue(); } @Test public void canSetImage_withNotFailedPrimary_andNullParent_returnsTrue() { assertThat(coordinator.canSetImage(primary)).isTrue(); } @Test public void canSetImage_withNotFailedPrimary_parentCanSetImage_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canSetImage(coordinator)).thenReturn(true); assertThat(coordinator.canSetImage(primary)).isTrue(); } @Test public void canSetImage_withNotFailedPrimary_parentCanNotSetImage_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); assertThat(coordinator.canSetImage(primary)).isFalse(); } @Test public void canSetImage_withError_andFailedPrimary_nullParent_returnsTrue() { coordinator.onRequestFailed(primary); assertThat(coordinator.canSetImage(error)).isTrue(); } @Test public void canSetImage_withError_andFailedPrimary_nonNullParentCanSetImage_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canSetImage(coordinator)).thenReturn(true); coordinator.onRequestFailed(primary); assertThat(coordinator.canSetImage(error)).isTrue(); } @Test public void canSetImage_withError_andFailedPrimary_nonNullParentCanNotSetImage_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); assertThat(coordinator.canSetImage(error)).isFalse(); } @Test public void canNotifyStatusChanged_withNotFailedPrimary_nullParent_returnsTrue() { assertThat(coordinator.canNotifyStatusChanged(primary)).isTrue(); } @Test public void canNotifyStatusChanged_withNotFailedPrimary_nonNullParentCantNotify_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); assertThat(coordinator.canNotifyStatusChanged(primary)).isFalse(); } @Test public void canNotifyStatusChanged_withNotFailedPrimary_nonNullParentCanNotify_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canNotifyStatusChanged(coordinator)).thenReturn(true); assertThat(coordinator.canNotifyStatusChanged(primary)).isTrue(); } @Test public void canNotifyStatusChanged_withError_notFailedPrimary_nullParent_returnsFalse() { assertThat(coordinator.canNotifyStatusChanged(error)).isFalse(); } @Test public void canNotifyStatusChanged_withErrorRequest_failedPrimary_nullParent_errorIsNotFailed_returnsFalse() { coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyStatusChanged(error)).isFalse(); } @Test public void canNotifyStatusChanged_withErrorRequest_failedPrimary_nullParent_failedError_returnsTrue() { coordinator.onRequestFailed(primary); coordinator.onRequestFailed(error); assertThat(coordinator.canNotifyStatusChanged(error)).isTrue(); } @Test public void canNotifyStatusChanged_withError_failedPrimary_nonNullParentCantNotify_false() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyStatusChanged(error)).isFalse(); } @Test public void canNotifyStatusChanged_withError_failedPrimary_notFailedError_nonNullParentCanNotify_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); when(parent.canNotifyStatusChanged(coordinator)).thenReturn(true); assertThat(coordinator.canNotifyStatusChanged(error)).isFalse(); } @Test public void canNotifyStatusChanged_withError_failedPrimary_failedError_nonNullParentCanNotify_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); when(parent.canNotifyStatusChanged(coordinator)).thenReturn(true); coordinator.onRequestFailed(error); assertThat(coordinator.canNotifyStatusChanged(error)).isTrue(); } @Test public void isAnyResourceSet_primaryNotSet_nullParent_returnsFalse() { assertThat(coordinator.isAnyResourceSet()).isFalse(); } @Test public void isAnyResourceSet_primarySet_nullParent_returnsTrue() { when(primary.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(primary); assertThat(coordinator.isAnyResourceSet()).isTrue(); } @Test public void isAnyResourceSet_primarySet_parentResourceNotSet_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(primary.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(primary); assertThat(coordinator.isAnyResourceSet()).isTrue(); } @Test public void isAnyResourceSet_primarySet_parentSet_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(primary.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(primary); when(parent.isAnyResourceSet()).thenReturn(true); assertThat(coordinator.isAnyResourceSet()).isTrue(); } @Test public void isAnyResourceSet_parentSet_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.isAnyResourceSet()).thenReturn(true); assertThat(coordinator.isAnyResourceSet()).isFalse(); } @Test public void isAnyResourceSet_errorSet_failedPrimary_nullParent_returnsTrue() { coordinator.onRequestFailed(primary); when(error.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(error); assertThat(coordinator.isAnyResourceSet()).isTrue(); } @Test public void isAnyResourceSet_errorSet_failedPrimary_nonNullParentNotSet_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); when(error.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(error); assertThat(coordinator.isAnyResourceSet()).isTrue(); } @Test public void isAnyResourceSet_errorSet_nonNullParentSet_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.isAnyResourceSet()).thenReturn(true); when(error.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(error); assertThat(coordinator.isAnyResourceSet()).isTrue(); } @Test public void isAnyResourceSet_primaryNotSet_errorNotSet_nonNullParentNotSet_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); assertThat(coordinator.isAnyResourceSet()).isFalse(); } @Test public void isAnyResourceSet_primaryNotSet_errorNotSet_nonNullParentSet_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.isAnyResourceSet()).thenReturn(true); assertThat(coordinator.isAnyResourceSet()).isFalse(); } @Test public void onRequestSuccess_nullParent_doesNotThrow() { coordinator.onRequestSuccess(primary); } @Test public void onRequestSuccess_nonNullParent_callsParent() { coordinator = newCoordinator(parent); coordinator.onRequestSuccess(primary); verify(parent).onRequestSuccess(coordinator); } @Test public void onRequestFailed_primaryRequest_notRunningError_beingsError() { coordinator.onRequestFailed(primary); verify(error).begin(); } @Test public void onRequestFailed_errorRequest_doesNotBeginError() { coordinator.onRequestFailed(error); verify(error, never()).begin(); } @Test public void onRequestFailed_primaryRequest_notRunningError_nonNullParent_doesNotNotifyParent() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); verify(parent, never()).onRequestFailed(any(Request.class)); } @Test public void onRequestFailed_errorRequest_nonNullParent_notifiesParent() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(error); verify(parent).onRequestFailed(coordinator); } @Test public void onRequestFailed_primaryRequest_runningError_nonNullParent_doesNotNotifyParent() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); coordinator.onRequestFailed(primary); verify(parent, never()).onRequestFailed(any(Request.class)); } @Test public void canNotifyCleared_primaryRequest_nullParent_returnsTrue() { assertThat(coordinator.canNotifyCleared(primary)).isTrue(); } @Test public void canNotifyCleared_primaryRequest_parentCanNotNotify_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); assertThat(coordinator.canNotifyCleared(primary)).isFalse(); } @Test public void canNotifyCleared_primaryRequest_parentCanNotify_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canNotifyCleared(coordinator)).thenReturn(true); assertThat(coordinator.canNotifyCleared(primary)).isTrue(); } @Test public void canNotifyCleared_primaryRequestFailed_parentCanNotify_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canNotifyCleared(coordinator)).thenReturn(true); coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(primary)).isTrue(); } @Test public void canNotifyCleared_primaryRequestFailed_parentCanNotNotify_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(primary)).isFalse(); } @Test public void canNotifyCleared_primaryRequestFailed_nullParent_returnsTrue() { coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(primary)).isTrue(); } @Test public void canNotifyCleared_errorRequest_nullParent_returnsFalse() { assertThat(coordinator.canNotifyCleared(error)).isFalse(); } @Test public void canNotifyCleared_errorRequest_primaryFailed_nullParent_returnsFalse() { coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(error)).isFalse(); } @Test public void canNotifyCleared_primaryRequest_primaryFailed_nonNullParentCanNotNotify_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canNotifyCleared(coordinator)).thenReturn(false); coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(primary)).isFalse(); } @Test public void canNotifyCleared_errorRequest_primaryFailed_nonNullParentCanNotNotify_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canNotifyCleared(coordinator)).thenReturn(false); coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(error)).isFalse(); } @Test public void canNotifyCleared_errorRequest_primaryFailed_nonNullParentCanNotify_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canNotifyCleared(coordinator)).thenReturn(true); coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(error)).isFalse(); } @Test public void canNotifyCleared_primaryRequest_primaryFailed_nonNullParentCanNotify_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(primary, error); when(parent.canNotifyCleared(coordinator)).thenReturn(true); coordinator.onRequestFailed(primary); assertThat(coordinator.canNotifyCleared(primary)).isTrue(); } private static ErrorRequestCoordinator newCoordinator() { return newCoordinator(/* parent= */ null); } private static ErrorRequestCoordinator newCoordinator(@Nullable RequestCoordinator parent) { return new ErrorRequestCoordinator(/* requestLock= */ new Object(), parent); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/RequestFutureTargetTest.java ================================================ package com.bumptech.glide.request; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.request.target.SizeReadyCallback; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class RequestFutureTargetTest { private int width; private int height; private RequestFutureTarget future; private Request request; private RequestFutureTarget.Waiter waiter; @Before public void setUp() { width = 100; height = 100; waiter = mock(RequestFutureTarget.Waiter.class); future = new RequestFutureTarget<>(width, height, false, waiter); request = mock(Request.class); future.setRequest(request); } @Test public void testCallsSizeReadyCallbackOnGetSize() { SizeReadyCallback cb = mock(SizeReadyCallback.class); future.getSize(cb); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testReturnsFalseForDoneBeforeDone() { assertFalse(future.isDone()); } @Test public void testReturnsTrueFromIsDoneIfDone() { future.onResourceReady( /* resource= */ new Object(), /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); assertTrue(future.isDone()); } @Test public void testReturnsFalseForIsCancelledBeforeCancelled() { assertFalse(future.isCancelled()); } @Test public void testReturnsTrueFromCancelIfNotYetDone() { assertTrue(future.cancel(false)); } @Test public void cancel_withMayInterruptIfRunningTrueAndNotFinishedRequest_clearsFuture() { future.cancel(true); verify(request).clear(); } @Test public void cancel_withInterruptFalseAndNotFinishedRequest_doesNotClearFuture() { future.cancel(false); verify(request, never()).clear(); } @Test public void testDoesNotRepeatedlyClearRequestIfCancelledRepeatedly() { future.cancel(true); future.cancel(true); verify(request, times(1)).clear(); } @Test public void testDoesNotClearRequestIfCancelledAfterDone() { future.onResourceReady( /* resource= */ new Object(), /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); future.cancel(true); verify(request, never()).clear(); } @Test public void testReturnsTrueFromDoneIfCancelled() { future.cancel(true); assertTrue(future.isDone()); } @Test public void testReturnsFalseFromIsCancelledIfCancelledAfterDone() { future.onResourceReady( /* resource= */ new Object(), /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); future.cancel(true); assertFalse(future.isCancelled()); } @Test public void testReturnsTrueFromCancelIfCancelled() { future.cancel(true); assertTrue(future.isCancelled()); } @Test public void testReturnsFalseFromCancelIfDone() { future.onResourceReady( /* resource= */ new Object(), /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); assertFalse(future.cancel(true)); } @Test public void testReturnsResourceOnGetIfAlreadyDone() throws ExecutionException, InterruptedException { Object expected = new Object(); future.onResourceReady( /* resource= */ expected, /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); assertEquals(expected, future.get()); } @Test public void testReturnsResourceOnGetWithTimeoutIfAlreadyDone() throws InterruptedException, ExecutionException, TimeoutException { Object expected = new Object(); future.onResourceReady( /* resource= */ expected, /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); assertEquals(expected, future.get(1, TimeUnit.MILLISECONDS)); } @Test(expected = CancellationException.class) public void testThrowsCancellationExceptionIfCancelledBeforeGet() throws ExecutionException, InterruptedException { future.cancel(true); future.get(); } @Test(expected = CancellationException.class) public void testThrowsCancellationExceptionIfCancelledBeforeGetWithTimeout() throws InterruptedException, ExecutionException, TimeoutException { future.cancel(true); future.get(100, TimeUnit.MILLISECONDS); } @Test(expected = ExecutionException.class) public void testThrowsExecutionExceptionOnGetIfExceptionBeforeGet() throws ExecutionException, InterruptedException { future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true); future.get(); } @Test(expected = ExecutionException.class) public void testThrowsExecutionExceptionOnGetIfExceptionWithNullValueBeforeGet() throws ExecutionException, InterruptedException, TimeoutException { future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true); future.get(100, TimeUnit.MILLISECONDS); } @Test(expected = ExecutionException.class) public void testThrowsExecutionExceptionOnGetIfExceptionBeforeGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException { future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true); future.get(100, TimeUnit.MILLISECONDS); } @Test(expected = TimeoutException.class) public void testThrowsTimeoutExceptionOnGetIfFailedToReceiveResourceInTime() throws InterruptedException, ExecutionException, TimeoutException { future.get(1, TimeUnit.MILLISECONDS); } @Test(expected = IllegalArgumentException.class) public void testThrowsExceptionIfGetCalledOnMainThread() throws ExecutionException, InterruptedException { future = new RequestFutureTarget<>(width, height, true, waiter); future.get(); } @Test public void testGetSucceedsOnMainThreadIfDone() throws ExecutionException, InterruptedException { future = new RequestFutureTarget<>(width, height, true, waiter); future.onResourceReady( /* resource= */ new Object(), /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); future.get(); } @Test(expected = InterruptedException.class) public void testThrowsInterruptedExceptionIfThreadInterruptedWhenDoneWaiting() throws InterruptedException, ExecutionException { doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { Thread.currentThread().interrupt(); return null; } }) .when(waiter) .waitForTimeout(eq(future), anyLong()); future.get(); } @Test(expected = ExecutionException.class) public void testThrowsExecutionExceptionIfLoadFailsWhileWaiting() throws ExecutionException, InterruptedException { doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { future.onLoadFailed( /* e= */ null, /* model= */ null, future, /* isFirstResource= */ true); return null; } }) .when(waiter) .waitForTimeout(eq(future), anyLong()); future.get(); } @Test(expected = CancellationException.class) public void testThrowsCancellationExceptionIfCancelledWhileWaiting() throws ExecutionException, InterruptedException { doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { future.cancel(false); return null; } }) .when(waiter) .waitForTimeout(eq(future), anyLong()); future.get(); } @Test(expected = TimeoutException.class) public void testThrowsTimeoutExceptionIfFinishesWaitingWithTimeoutAndDoesNotReceiveResult() throws ExecutionException, InterruptedException, TimeoutException { future.get(1, TimeUnit.MILLISECONDS); } @Test(expected = AssertionError.class) public void testThrowsAssertionErrorIfFinishesWaitingWithoutTimeoutAndDoesNotReceiveResult() throws ExecutionException, InterruptedException { future.get(); } @Test public void testNotifiesAllWhenLoadFails() { future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true); verify(waiter).notifyAll(eq(future)); } @Test public void testNotifiesAllWhenResourceReady() { future.onResourceReady( /* resource= */ new Object(), /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); verify(waiter).notifyAll(eq(future)); } @Test public void testNotifiesAllOnCancelIfNotCancelled() { future.cancel(false); verify(waiter).notifyAll(eq(future)); } @Test public void testDoesNotNotifyAllOnSecondCancel() { future.cancel(true); verify(waiter).notifyAll(eq(future)); future.cancel(true); verify(waiter, times(1)).notifyAll(eq(future)); } @Test public void testReturnsResourceIfReceivedWhileWaiting() throws ExecutionException, InterruptedException { final Object expected = new Object(); doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) { future.onResourceReady( /* resource= */ expected, /* model= */ null, /* target= */ future, DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); return null; } }) .when(waiter) .waitForTimeout(eq(future), anyLong()); assertEquals(expected, future.get()); } @Test public void testWaitsForeverIfNoTimeoutSet() throws InterruptedException { try { future.get(); } catch (ExecutionException e) { throw new RuntimeException(e); } catch (AssertionError e) { // Expected. } verify(waiter).waitForTimeout(eq(future), eq(0L)); } @Test public void testWaitsForGivenTimeoutMillisIfTimeoutSet() throws InterruptedException { long timeout = 2; try { future.get(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } catch (TimeoutException e) { // Expected. } verify(waiter, atLeastOnce()).waitForTimeout(eq(future), eq(timeout)); } @Test public void testConvertsOtherTimeUnitsToMillisForWaiter() throws InterruptedException { long timeoutMicros = 1000; try { future.get(timeoutMicros, TimeUnit.MICROSECONDS); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } catch (TimeoutException e) { // Expected. } verify(waiter, atLeastOnce()) .waitForTimeout(eq(future), eq(TimeUnit.MICROSECONDS.toMillis(timeoutMicros))); } @Test public void testDoesNotWaitIfGivenTimeOutEqualToZero() throws InterruptedException { try { future.get(0, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } catch (TimeoutException e) { // Expected. } verify(waiter, never()).waitForTimeout(eq(future), anyLong()); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/RequestOptionsTest.java ================================================ package com.bumptech.glide.request; import static com.google.common.truth.Truth.assertThat; import android.app.Application; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.Priority; import com.bumptech.glide.load.MultiTransformation; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.CircleCrop; import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.util.Util; import com.google.common.testing.EqualsTester; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class RequestOptionsTest { private RequestOptions options; @Mock private Transformation transformation; private Application app; @Before public void setUp() { MockitoAnnotations.initMocks(this); options = new RequestOptions(); app = ApplicationProvider.getApplicationContext(); } @Test public void isScaleOnlyOrNoTransform_byDefault_isTrue() { assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withFitCenter_isTrue() { options.fitCenter(); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); options.optionalFitCenter(); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withCenterInside_isTrue() { options.centerInside(); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); options.optionalCenterInside(); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withCenterCrop_isFalse() { options.centerCrop(); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); options.optionalCenterCrop(); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); } @Test public void isScaleOnlyOrNoTransform_withCircleCrop_isFalse() { options.circleCrop(); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); options.circleCrop(); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); } @Test public void isScaleOnlyOrNoTransform_withBitmapTransformation_isFalse() { options.transform(transformation); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); options.optionalTransform(transformation); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); } @Test public void isScaleOnlyOrNoTransform_withCustomTransformation_isFalse() { options.transform(Bitmap.class, transformation); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); options.optionalTransform(Bitmap.class, transformation); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); } @Test public void isScaleOnlyOrNoTransform_withDownsampleStrategy_isTrue() { options.downsample(DownsampleStrategy.CENTER_OUTSIDE); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withNonScaleAndThenDontTransform_isTrue() { options.circleCrop().dontTransform(); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withNonScaleAndAppliedDontTransform_isTrue() { options.circleCrop(); options.apply(new RequestOptions().dontTransform()); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withDontTransformAndAppliedNonScaleTransform_isFalse() { options.fitCenter(); options.apply(new RequestOptions().circleCrop()); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); } @Test public void isScaleOnlyOrNoTransform_withNonScaleOnly_andAppliedWithScaleOnly_isTrue() { options.circleCrop(); options.apply(new RequestOptions().fitCenter()); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withScaleOnlyAndAppliedWithoutTransform_isTrue() { options.fitCenter(); options.apply(new RequestOptions().dontAnimate()); assertThat(options.isScaleOnlyOrNoTransform()).isTrue(); } @Test public void isScaleOnlyOrNoTransform_withNonScaleOnlyAndAppliedWithoutTransform_isFalse() { options.circleCrop(); options.apply(new RequestOptions().dontAnimate()); assertThat(options.isScaleOnlyOrNoTransform()).isFalse(); } @Test public void testIsTransformationRequired_byDefault_isFalse() { assertThat(options.isTransformationRequired()).isFalse(); } @Test public void testIsTransformationSet_byDefault_isFalse() { assertThat(options.isTransformationSet()).isFalse(); } @Test public void testIsTransformationAllowed_byDefault_isTrue() { assertThat(options.isTransformationAllowed()).isTrue(); } @Test public void testIsTransformationSet_afterApplyingOptionsWithTransform_isTrue() { RequestOptions other = new RequestOptions(); other.transform(Bitmap.class, transformation); options.apply(other); assertThat(options.isTransformationSet()).isTrue(); } @Test public void testIsTransformationSet_afterDontTransform_isFalse() { options.dontTransform(); assertThat(options.isTransformationSet()).isFalse(); } @Test public void testIsTransformationAllowed_afterDontTransform_isFalse() { options.dontTransform(); assertThat(options.isTransformationAllowed()).isFalse(); } @Test public void testIsTransformationRequired_afterDontTransform_isFalse() { options.dontTransform(); assertThat(options.isTransformationRequired()).isFalse(); } @Test public void testApplyingDontTransform_overridesTransformations() { options.transform(transformation); options.dontTransform(); assertThat(options.isTransformationSet()).isFalse(); assertThat(options.isTransformationRequired()).isFalse(); assertThat(options.getTransformations()).isEmpty(); } @Test public void testApplyingTransformation_overridesDontTransform() { options.dontTransform(); options.transform(transformation); assertThat(options.isTransformationAllowed()).isTrue(); assertThat(options.isTransformationRequired()).isTrue(); assertThat(options.getTransformations()).containsEntry(Bitmap.class, transformation); } @Test public void testApplyingOptions_withDontTransform_overridesTransformations() { options.transform(transformation); RequestOptions other = new RequestOptions(); other.dontTransform(); options.apply(other); assertThat(options.isTransformationAllowed()).isFalse(); assertThat(options.isTransformationSet()).isFalse(); assertThat(options.isTransformationRequired()).isFalse(); assertThat(options.getTransformations()).isEmpty(); } @Test public void testApplyingOptions_withTransformation_overridesDontTransform() { options.dontTransform(); RequestOptions other = new RequestOptions(); other.transform(transformation); options.apply(other); assertThat(options.isTransformationAllowed()).isTrue(); assertThat(options.isTransformationSet()).isTrue(); assertThat(options.isTransformationRequired()).isTrue(); assertThat(options.getTransformations()).containsEntry(Bitmap.class, transformation); } @Test public void testApplyingDefaultOptions_withDontTransform_retainsDontTransform() { options.dontTransform(); options.apply(new RequestOptions()); assertThat(options.isTransformationAllowed()).isFalse(); assertThat(options.isTransformationRequired()).isFalse(); assertThat(options.getTransformations()).isEmpty(); } @Test public void testApplyingDefaultOptions_withTransform_retrainsTransform() { options.transform(transformation); options.apply(new RequestOptions()); assertThat(options.isTransformationAllowed()).isTrue(); assertThat(options.isTransformationRequired()).isTrue(); assertThat(options.getTransformations()).containsEntry(Bitmap.class, transformation); } @Test @SuppressWarnings({"unchecked", "varargs"}) public void testApplyMultiTransform() { options.transform(new CircleCrop(), new CenterCrop()); assertThat(options.isTransformationRequired()).isTrue(); assertThat(options.getTransformations()).containsKey(Bitmap.class); assertThat(options.getTransformations().get(Bitmap.class)) .isInstanceOf(MultiTransformation.class); } @Test public void isSkipMemoryCacheSet_withoutSkipMemoryCache_isFalse() { assertThat(options.isSkipMemoryCacheSet()).isFalse(); } @Test public void isSkipMemoryCacheSet_withSkipMemoryCacheTrue_isTrue() { assertThat(options.skipMemoryCache(true).isSkipMemoryCacheSet()).isTrue(); } @Test public void isSkipMemoryCacheSet_withSkipMemoryCacheFalse_isTrue() { assertThat(options.skipMemoryCache(false).isSkipMemoryCacheSet()).isTrue(); } @Test public void isDiskCacheStrategySet_withoutDiskCacheStrategy_isFalse() { assertThat(options.isDiskCacheStrategySet()).isFalse(); } @Test public void isDiskCacheStrategySet_withDiskCacheStrategyDefault_isTrue() { assertThat(options.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).isDiskCacheStrategySet()) .isTrue(); } @Test public void isDiskCacheStrategySet_withDiskCacheStrategyNonDefault_isTrue() { assertThat(options.diskCacheStrategy(DiskCacheStrategy.ALL).isDiskCacheStrategySet()).isTrue(); } @Test public void getPlaceholder_afterSettingPlaceholderId_returnsNul() { assertThat( options .placeholder(new ColorDrawable(Color.RED)) .placeholder(android.R.drawable.star_on) .getPlaceholderDrawable()) .isNull(); } @Test public void getPlaceholder_afterApplyingOptionsWithPlaceholderId_returnsNull() { RequestOptions toApply = new RequestOptions().placeholder(android.R.drawable.star_on); assertThat( options .placeholder(new ColorDrawable(Color.RED)) .apply(toApply) .getPlaceholderDrawable()) .isNull(); } @Test public void getPlaceholder_afterApplyingOptionsWithPlaceholderDrawable_returnsNewDrawable() { Drawable expected = new ColorDrawable(Color.GREEN); RequestOptions toApply = new RequestOptions().placeholder(expected); assertThat( options .placeholder(new ColorDrawable(Color.RED)) .apply(toApply) .getPlaceholderDrawable()) .isEqualTo(expected); } /** * Verifies that we set the flags for placeholder id correctly when applying a placeholder id via * another RequestOptions. */ @Test public void placeholderIdFlag_afterApplyingIdViaOtherRequestOptions_isSet() { assertThat( options .placeholder(new ColorDrawable(Color.RED)) .apply( new RequestOptions() .apply(new RequestOptions().placeholder(android.R.drawable.star_on))) .getPlaceholderDrawable()) .isNull(); } @Test public void getPlaceholderId_afterSettingPlaceholderDrawable_returnsZero() { assertThat( options .placeholder(android.R.drawable.star_on) .placeholder(new ColorDrawable(Color.RED)) .getPlaceholderId()) .isEqualTo(0); } @Test public void getPlaceholderId_afterApplyingOptionsWithPlaceholderDrawable_returnsZero() { RequestOptions toApply = new RequestOptions().placeholder(new ColorDrawable(Color.RED)); assertThat(options.placeholder(android.R.drawable.star_on).apply(toApply).getPlaceholderId()) .isEqualTo(0); } @Test public void getPlaceholderId_afterApplyingOptionsWithId_returnsNewId() { int expectedId = android.R.drawable.star_off; RequestOptions toApply = new RequestOptions().placeholder(expectedId); assertThat(options.placeholder(android.R.drawable.star_on).apply(toApply).getPlaceholderId()) .isEqualTo(expectedId); } /** * Verifies that we set the flags for placeholder correctly when applying a placeholder via * another RequestOptions. */ @Test public void placeholderFlag_afterApplyingViaOtherRequestOptions_isSet() { assertThat( options .placeholder(android.R.drawable.star_on) .apply( new RequestOptions() .apply(new RequestOptions().placeholder(new ColorDrawable(Color.RED)))) .getPlaceholderId()) .isEqualTo(0); } @Test public void getFallback_afterSettingFallbackId_returnsNull() { assertThat( options .fallback(new ColorDrawable(Color.RED)) .fallback(android.R.drawable.star_on) .getFallbackDrawable()) .isNull(); } @Test public void getFallback_afterApplyingOptionsWithFallbackId_returnsNull() { RequestOptions toApply = new RequestOptions().fallback(android.R.drawable.star_on); assertThat(options.fallback(new ColorDrawable(Color.RED)).apply(toApply).getFallbackDrawable()) .isNull(); } @Test public void getFallback_afterApplyingOptionsWithFallbackDrawable_returnsNewDrawable() { RequestOptions toApply = new RequestOptions(); RequestOptions apply = options.fallback(new ColorDrawable(Color.RED)).apply(toApply); assertThat(((ColorDrawable) apply.getFallbackDrawable()).getColor()).isEqualTo(Color.RED); } /** * Verifies that we set the flags for fallback correctly when applying a fallback via another * RequestOptions. */ @Test public void fallbackFlag_afterApplyingViaOtherRequestOptions_isSet() { assertThat( options .fallback(android.R.drawable.star_on) .apply( new RequestOptions() .apply(new RequestOptions().fallback(new ColorDrawable(Color.RED)))) .getFallbackId()) .isEqualTo(0); } @Test public void getFallbackId_afterSettingFallbackDrawable_returnsZero() { assertThat( options .fallback(android.R.drawable.star_on) .fallback(new ColorDrawable(Color.RED)) .getFallbackId()) .isEqualTo(0); } @Test public void getFallbackId_afterApplyingOptionsWithFallbackDrawable_returnsZero() { RequestOptions toApply = new RequestOptions().fallback(new ColorDrawable(Color.RED)); assertThat(options.fallback(android.R.drawable.star_on).apply(toApply).getFallbackId()) .isEqualTo(0); } @Test public void getFallbackId_afterApplyingOptionsWithFallbackId_returnsNewFallbackId() { RequestOptions toApply = new RequestOptions().fallback(android.R.drawable.star_off); assertThat(options.fallback(android.R.drawable.star_on).apply(toApply).getFallbackId()) .isEqualTo(android.R.drawable.star_off); } /** * Verifies that we set the flags for fallback id correctly when applying a fallback id via * another RequestOptions. */ @Test public void fallbackIdFlag_afterApplyingViaOtherRequestOptions_isSet() { assertThat( options .fallback(new ColorDrawable(Color.RED)) .apply( new RequestOptions() .apply(new RequestOptions().fallback(android.R.drawable.star_on))) .getFallbackDrawable()) .isNull(); } @Test public void getError_afterSettingErrorId_returnsNull() { assertThat( options .error(new ColorDrawable(Color.RED)) .error(android.R.drawable.star_on) .getErrorPlaceholder()) .isNull(); } @Test public void getError_afterApplyingOptionsWithErrorId_returnsNull() { RequestOptions toApply = new RequestOptions().error(android.R.drawable.star_on); assertThat(options.error(new ColorDrawable(Color.RED)).apply(toApply).getErrorPlaceholder()) .isNull(); } @Test public void getError_afterApplyingOptionsWithErrorDrawable_returnsNewErrorDrawable() { Drawable expected = new ColorDrawable(Color.GREEN); RequestOptions toApply = new RequestOptions().error(expected); assertThat(options.error(new ColorDrawable(Color.RED)).apply(toApply).getErrorPlaceholder()) .isEqualTo(expected); } /** * Verifies that we set the flags for error correctly when applying an error via another * RequestOptions. */ @Test public void errorFlag_afterApplyingViaOtherRequestOptions_isSet() { assertThat( options .error(android.R.drawable.star_on) .apply( new RequestOptions() .apply(new RequestOptions().error(new ColorDrawable(Color.RED)))) .getErrorId()) .isEqualTo(0); } @Test public void getErrorId_afterSettingErrorDrawable_returnsZero() { assertThat( options .error(android.R.drawable.star_on) .error(new ColorDrawable(Color.RED)) .getErrorId()) .isEqualTo(0); } @Test public void getErrorId_afterApplyingOptionsWithErrorDrawable_returnsZero() { RequestOptions toApply = new RequestOptions().error(new ColorDrawable(Color.RED)); assertThat(options.error(android.R.drawable.star_on).apply(toApply).getErrorId()).isEqualTo(0); } @Test public void getErrorId_afterApplyingOptionsWithErrorId_returnsNewErrorId() { RequestOptions toApply = new RequestOptions().error(android.R.drawable.star_off); assertThat(options.error(android.R.drawable.star_on).apply(toApply).getErrorId()) .isEqualTo(android.R.drawable.star_off); } /** * Verifies that we set the flags for error id correctly when applying a fallback id via another * RequestOptions. */ @Test public void errorIdFlag_afterApplyingViaOtherRequestOptions_isSet() { assertThat( options .error(new ColorDrawable(Color.RED)) .apply( new RequestOptions() .apply(new RequestOptions().error(android.R.drawable.star_on))) .getErrorPlaceholder()) .isNull(); } @Test public void testEqualsHashCode() { Drawable first = new ColorDrawable(Color.RED); Drawable second = new GradientDrawable(); assertThat(first).isNotEqualTo(second); assertThat(Util.bothNullOrEqual(first, second)).isFalse(); // Make sure we're not equal to any other subclass of RequestOptions. class FakeOptions extends BaseRequestOptions { @Override public boolean equals(Object o) { return o instanceof FakeOptions && super.equals(o); } // Our class doesn't include any additional properties, so we don't need to modify hashcode, // but // keep it here as a reminder in case we add properties. @SuppressWarnings("PMD.UselessOverridingMethod") @Override public int hashCode() { return super.hashCode(); } } new EqualsTester() .addEqualityGroup( new RequestOptions(), new RequestOptions(), new RequestOptions().skipMemoryCache(false), new RequestOptions().onlyRetrieveFromCache(false), new RequestOptions().useUnlimitedSourceGeneratorsPool(false)) .addEqualityGroup(new FakeOptions(), new FakeOptions()) .addEqualityGroup( new RequestOptions().sizeMultiplier(.7f), new RequestOptions().sizeMultiplier(.7f)) .addEqualityGroup(new RequestOptions().sizeMultiplier(0.8f)) .addEqualityGroup(new RequestOptions().error(1), new RequestOptions().error(1)) .addEqualityGroup(new RequestOptions().error(2)) .addEqualityGroup(new RequestOptions().error(first), new RequestOptions().error(first)) .addEqualityGroup(new RequestOptions().error(second)) .addEqualityGroup(new RequestOptions().placeholder(1), new RequestOptions().placeholder(1)) .addEqualityGroup(new RequestOptions().placeholder(2)) .addEqualityGroup( new RequestOptions().placeholder(first), new RequestOptions().placeholder(first)) .addEqualityGroup(new RequestOptions().placeholder(second)) .addEqualityGroup(new RequestOptions().fallback(1), new RequestOptions().fallback(1)) .addEqualityGroup(new RequestOptions().fallback(2)) .addEqualityGroup( new RequestOptions().fallback(first), new RequestOptions().fallback(first)) .addEqualityGroup(new RequestOptions().fallback(second)) .addEqualityGroup( new RequestOptions().skipMemoryCache(true), new RequestOptions().skipMemoryCache(true)) .addEqualityGroup( new RequestOptions().override(100), new RequestOptions().override(100, 100)) .addEqualityGroup( new RequestOptions().override(200), new RequestOptions().override(200, 200)) .addEqualityGroup( new RequestOptions().override(100, 200), new RequestOptions().override(100, 200)) .addEqualityGroup( new RequestOptions().override(200, 100), new RequestOptions().override(200, 100)) .addEqualityGroup(new RequestOptions().centerCrop(), new RequestOptions().centerCrop()) .addEqualityGroup( new RequestOptions().optionalCenterCrop(), new RequestOptions().optionalCenterCrop()) .addEqualityGroup(new RequestOptions().fitCenter()) .addEqualityGroup(new RequestOptions().circleCrop()) .addEqualityGroup(new RequestOptions().centerInside()) .addEqualityGroup( new RequestOptions().useUnlimitedSourceGeneratorsPool(true), new RequestOptions().useUnlimitedSourceGeneratorsPool(true)) .addEqualityGroup( new RequestOptions().onlyRetrieveFromCache(true), new RequestOptions().onlyRetrieveFromCache(true)) .addEqualityGroup( new RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL), new RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL)) .addEqualityGroup(new RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE)) .addEqualityGroup( new RequestOptions().priority(Priority.HIGH), new RequestOptions().priority(Priority.HIGH)) .addEqualityGroup(new RequestOptions().priority(Priority.LOW)) .addEqualityGroup( new RequestOptions().set(Option.memory("test"), true), new RequestOptions().set(Option.memory("test"), true)) .addEqualityGroup(new RequestOptions().set(Option.memory("test"), false)) .addEqualityGroup(new RequestOptions().set(Option.memory("test2"), true)) .addEqualityGroup( new RequestOptions().decode(Integer.class), new RequestOptions().decode(Integer.class)) .addEqualityGroup(new RequestOptions().decode(Float.class)) .addEqualityGroup( new RequestOptions().signature(new ObjectKey("test")), new RequestOptions().signature(new ObjectKey("test"))) .addEqualityGroup(new RequestOptions().signature(new ObjectKey("test2"))) .addEqualityGroup( new RequestOptions().theme(app.getTheme()), new RequestOptions().theme(app.getTheme())) .testEquals(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/SingleRequestTest.java ================================================ package com.bumptech.glide.request; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.bumptech.glide.tests.Util.isADataSource; import static com.bumptech.glide.tests.Util.mockResource; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.GlideContext; import com.bumptech.glide.GlideExperiments; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.request.transition.TransitionFactory; import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.util.Executors; import com.google.common.base.Equivalence; import com.google.common.testing.EquivalenceTester; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) @SuppressWarnings("rawtypes") public class SingleRequestTest { private SingleRequestBuilder builder; @Mock private ExperimentalRequestListener listener1; @Mock private RequestListener listener2; @Before public void setUp() { MockitoAnnotations.initMocks(this); builder = new SingleRequestBuilder(); } @Test public void testIsNotCompleteBeforeReceivingResource() { SingleRequest request = builder.build(); assertFalse(request.isComplete()); } @Test public void testCanHandleNullResources() { SingleRequest request = builder.addRequestListener(listener1).build(); request.onResourceReady(null, DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onLoadFailed(isAGlideException(), isA(Number.class), eq(builder.target), anyBoolean()); } @Test public void testCanHandleEmptyResources() { SingleRequest request = builder.addRequestListener(listener1).build(); when(builder.resource.get()).thenReturn(null); request.onResourceReady( builder.resource, DataSource.REMOTE, /* isLoadedFromAlternateCacheKey= */ false); verify(builder.engine).release(eq(builder.resource)); verify(listener1) .onLoadFailed(isAGlideException(), any(Number.class), eq(builder.target), anyBoolean()); } @Test public void testCanHandleNonConformingResources() { SingleRequest request = builder.addRequestListener(listener1).build(); when(((Resource) (builder.resource)).get()) .thenReturn("Invalid mocked String, this should be a List"); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ true); verify(builder.engine).release(eq(builder.resource)); verify(listener1) .onLoadFailed(isAGlideException(), any(Number.class), eq(builder.target), anyBoolean()); } @Test public void testIsCompleteAfterReceivingResource() { SingleRequest request = builder.build(); request.onResourceReady( builder.resource, DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); assertTrue(request.isComplete()); } @Test public void testIsNotCompleteAfterClear() { SingleRequest request = builder.build(); request.onResourceReady( builder.resource, DataSource.REMOTE, /* isLoadedFromAlternateCacheKey= */ false); request.clear(); assertFalse(request.isComplete()); } @Test public void testIsCancelledAfterClear() { SingleRequest request = builder.build(); request.clear(); assertTrue(request.isCleared()); } @Test public void clear_notifiesTarget() { SingleRequest request = builder.build(); request.clear(); verify(builder.target).onLoadCleared(anyDrawableOrNull()); } @Test public void testDoesNotNotifyTargetTwiceIfClearedTwiceInARow() { SingleRequest request = builder.build(); request.clear(); request.clear(); verify(builder.target, times(1)).onLoadCleared(anyDrawableOrNull()); } @Test public void clear_doesNotNotifyTarget_ifRequestCoordinatorReturnsFalseForCanClear() { when(builder.requestCoordinator.canNotifyCleared(any(Request.class))).thenReturn(false); SingleRequest request = builder.build(); request.clear(); verify(builder.target, never()).onLoadCleared(any(Drawable.class)); } @Test public void testResourceIsNotCompleteWhenAskingCoordinatorIfCanSetImage() { RequestCoordinator requestCoordinator = mock(RequestCoordinator.class); when(requestCoordinator.getRoot()).thenReturn(requestCoordinator); doAnswer( new Answer() { @Override public Object answer(InvocationOnMock invocation) { Request request = (Request) invocation.getArguments()[0]; assertFalse(request.isComplete()); return true; } }) .when(requestCoordinator) .canSetImage(any(Request.class)); SingleRequest request = builder.setRequestCoordinator(requestCoordinator).build(); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); verify(requestCoordinator).canSetImage(eq(request)); } @Test public void pause_whenRequestIsWaitingForASize_clearsRequest() { SingleRequest request = builder.build(); request.begin(); request.pause(); assertThat(request.isRunning()).isFalse(); assertThat(request.isCleared()).isTrue(); } @Test public void pause_whenRequestIsWaitingForAResource_clearsRequest() { SingleRequest request = builder.build(); request.begin(); request.onSizeReady(100, 100); request.pause(); assertThat(request.isRunning()).isFalse(); assertThat(request.isCleared()).isTrue(); } @Test public void pause_whenComplete_doesNotClearRequest() { SingleRequest request = builder.build(); request.onResourceReady( builder.resource, DataSource.REMOTE, /* isLoadedFromAlternateCacheKey= */ false); request.pause(); assertThat(request.isComplete()).isTrue(); } @Test public void pause_whenCleared_doesNotClearRequest() { SingleRequest request = builder.build(); request.clear(); request.pause(); verify(builder.target, times(1)).onLoadCleared(anyDrawableOrNull()); } @Test public void testIgnoresOnSizeReadyIfNotWaitingForSize() { SingleRequest request = builder.build(); request.begin(); request.onSizeReady(100, 100); request.onSizeReady(100, 100); verify(builder.engine, times(1)) .load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), eq(100), eq(100), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), anyBoolean(), /* useAnimationPool= */ anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor()); } @Test public void testEngineLoadCancelledOnCancel() { Engine.LoadStatus loadStatus = mock(Engine.LoadStatus.class); when(builder.engine.load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), anyInt(), anyInt(), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor())) .thenReturn(loadStatus); SingleRequest request = builder.build(); request.begin(); request.onSizeReady(100, 100); request.clear(); verify(loadStatus).cancel(); } @Test public void testResourceIsRecycledOnClear() { SingleRequest request = builder.build(); request.onResourceReady( builder.resource, DataSource.REMOTE, /* isLoadedFromAlternateCacheKey= */ false); request.clear(); verify(builder.engine).release(eq(builder.resource)); } @Test public void testPlaceholderDrawableIsSet() { Drawable expected = new ColorDrawable(Color.RED); MockTarget target = new MockTarget(); SingleRequest request = builder.setPlaceholderDrawable(expected).setTarget(target).build(); request.begin(); assertThat(target.currentPlaceholder).isEqualTo(expected); } @Test public void testErrorDrawableIsSetOnLoadFailed() { Drawable expected = new ColorDrawable(Color.RED); MockTarget target = new MockTarget(); SingleRequest request = builder.setErrorDrawable(expected).setTarget(target).build(); request.onLoadFailed(new GlideException("test")); assertThat(target.currentPlaceholder).isEqualTo(expected); } @Test public void testPlaceholderDrawableSetOnNullModelWithNoErrorDrawable() { Drawable placeholder = new ColorDrawable(Color.RED); MockTarget target = new MockTarget(); SingleRequest request = builder.setErrorDrawable(placeholder).setTarget(target).setModel(null).build(); request.begin(); assertThat(target.currentPlaceholder).isEqualTo(placeholder); } @Test public void testErrorDrawableSetOnNullModelWithErrorDrawable() { Drawable placeholder = new ColorDrawable(Color.RED); Drawable errorPlaceholder = new ColorDrawable(Color.GREEN); MockTarget target = new MockTarget(); SingleRequest request = builder .setPlaceholderDrawable(placeholder) .setErrorDrawable(errorPlaceholder) .setTarget(target) .setModel(null) .build(); request.begin(); assertThat(target.currentPlaceholder).isEqualTo(errorPlaceholder); } @Test public void testFallbackDrawableSetOnNullModelWithErrorAndFallbackDrawables() { Drawable placeholder = new ColorDrawable(Color.RED); Drawable errorPlaceholder = new ColorDrawable(Color.GREEN); Drawable fallback = new ColorDrawable(Color.BLUE); MockTarget target = new MockTarget(); SingleRequest request = builder .setPlaceholderDrawable(placeholder) .setErrorDrawable(errorPlaceholder) .setFallbackDrawable(fallback) .setTarget(target) .setModel(null) .build(); request.begin(); assertThat(target.currentPlaceholder).isEqualTo(fallback); } @Test public void testIsNotRunningBeforeRunCalled() { assertFalse(builder.build().isRunning()); } @Test public void testIsRunningAfterRunCalled() { Request request = builder.build(); request.begin(); assertTrue(request.isRunning()); } @Test public void testIsNotRunningAfterComplete() { SingleRequest request = builder.build(); request.begin(); request.onResourceReady( builder.resource, DataSource.REMOTE, /* isLoadedFromAlternateCacheKey= */ false); assertFalse(request.isRunning()); } @Test public void testIsNotRunningAfterFailing() { SingleRequest request = builder.build(); request.begin(); request.onLoadFailed(new GlideException("test")); assertFalse(request.isRunning()); } @Test public void testIsNotRunningAfterClear() { SingleRequest request = builder.build(); request.begin(); request.clear(); assertFalse(request.isRunning()); } @Test public void testCallsTargetOnResourceReadyIfNoRequestListener() { SingleRequest request = builder.build(); request.onResourceReady( builder.resource, DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); verify(builder.target).onResourceReady(eq(builder.result), anyTransition()); } @Test public void testCallsTargetOnResourceReadyIfAllRequestListenersReturnFalse() { SingleRequest request = builder.addRequestListener(listener1).addRequestListener(listener2).build(); when(listener1.onResourceReady( any(List.class), any(Number.class), eq(builder.target), isADataSource(), anyBoolean())) .thenReturn(false); when(listener2.onResourceReady( any(List.class), any(Number.class), eq(builder.target), isADataSource(), anyBoolean())) .thenReturn(false); request.onResourceReady( builder.resource, DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); verify(builder.target).onResourceReady(eq(builder.result), anyTransition()); } @Test public void testDoesNotCallTargetOnResourceReadyIfAnyRequestListenerReturnsTrue() { SingleRequest request = builder.addRequestListener(listener1).addRequestListener(listener2).build(); when(listener1.onResourceReady( any(List.class), any(Number.class), eq(builder.target), isADataSource(), anyBoolean())) .thenReturn(false); when(listener1.onResourceReady( any(List.class), any(Number.class), eq(builder.target), isADataSource(), anyBoolean())) .thenReturn(true); request.onResourceReady( builder.resource, DataSource.REMOTE, /* isLoadedFromAlternateCacheKey= */ false); verify(builder.target, never()).onResourceReady(any(List.class), anyTransition()); } @Test public void testCallsTargetOnExceptionIfNoRequestListener() { SingleRequest request = builder.build(); request.onLoadFailed(new GlideException("test")); verify(builder.target).onLoadFailed(eq(builder.errorDrawable)); } @Test public void testCallsTargetOnExceptionIfAllRequestListenersReturnFalse() { SingleRequest request = builder.addRequestListener(listener1).addRequestListener(listener2).build(); when(listener1.onLoadFailed( isAGlideException(), any(Number.class), eq(builder.target), anyBoolean())) .thenReturn(false); when(listener2.onLoadFailed( isAGlideException(), any(Number.class), eq(builder.target), anyBoolean())) .thenReturn(false); request.onLoadFailed(new GlideException("test")); verify(builder.target).onLoadFailed(eq(builder.errorDrawable)); } @Test public void testDoesNotCallTargetOnExceptionIfAnyRequestListenerReturnsTrue() { SingleRequest request = builder.addRequestListener(listener1).addRequestListener(listener2).build(); when(listener1.onLoadFailed( isAGlideException(), any(Number.class), eq(builder.target), anyBoolean())) .thenReturn(false); when(listener2.onLoadFailed( isAGlideException(), any(Number.class), eq(builder.target), anyBoolean())) .thenReturn(true); request.onLoadFailed(new GlideException("test")); verify(builder.target, never()).onLoadFailed(any(Drawable.class)); } @Test public void testRequestListenerIsCalledWithResourceResult() { SingleRequest request = builder.addRequestListener(listener1).build(); boolean isLoadedFromAlternateCacheKey = true; request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, isLoadedFromAlternateCacheKey); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), anyBoolean()); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), anyBoolean(), eq(isLoadedFromAlternateCacheKey)); } @Test public void testRequestListenerIsCalledWithModel() { SingleRequest request = builder.addRequestListener(listener1).build(); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady( any(List.class), eq(builder.model), isAListTarget(), isADataSource(), anyBoolean()); } @Test public void testRequestListenerIsCalledWithTarget() { SingleRequest request = builder.addRequestListener(listener1).build(); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady( any(List.class), any(Number.class), eq(builder.target), isADataSource(), anyBoolean()); } @Test public void testRequestListenerIsCalledWithLoadedFromMemoryIfLoadCompletesSynchronously() { final SingleRequest request = builder.addRequestListener(listener1).build(); when(builder.engine.load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), anyInt(), anyInt(), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), anyBoolean(), /* useAnimationPool= */ anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor())) .thenAnswer( new Answer() { @Override public Object answer(InvocationOnMock invocation) { request.onResourceReady( builder.resource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false); return null; } }); request.begin(); request.onSizeReady(100, 100); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), eq(DataSource.MEMORY_CACHE), anyBoolean()); } @Test public void testRequestListenerIsCalledWithNotLoadedFromMemoryCacheIfLoadCompletesAsynchronously() { SingleRequest request = builder.addRequestListener(listener1).build(); request.onSizeReady(100, 100); request.onResourceReady( builder.resource, DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), eq(DataSource.LOCAL), anyBoolean()); } @Test public void testRequestListenerIsCalledWithIsFirstResourceIfNoRequestCoordinator() { SingleRequest request = builder.setRequestCoordinator(null).addRequestListener(listener1).build(); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(true)); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(true), eq(false)); } @Test public void testRequestListenerIsCalledWithFirstImageIfRequestCoordinatorReturnsNoResourceSet() { SingleRequest request = builder.addRequestListener(listener1).build(); when(builder.requestCoordinator.isAnyResourceSet()).thenReturn(false); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(true)); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(true), eq(false)); } @Test public void testRequestListenerIsCalledWithNotIsFirstRequestIfRequestCoordinatorReturnsResourceSet() { SingleRequest request = builder.addRequestListener(listener1).build(); when(builder.requestCoordinator.isAnyResourceSet()).thenReturn(true); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(false)); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(false), eq(false)); } @Test public void onResourceReady_notifiesRequestCoordinator_beforeCallingRequestListeners() { AtomicBoolean isRequestCoordinatorVerified = new AtomicBoolean(); SingleRequest request = builder .setTarget(new DoNothingTarget()) .addRequestListener( new RequestListener<>() { @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady( @NonNull List resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { verify(builder.requestCoordinator).onRequestSuccess(target.getRequest()); isRequestCoordinatorVerified.set(true); return false; } }) .build(); builder.target.setRequest(request); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); assertThat(isRequestCoordinatorVerified.get()).isTrue(); } @Test public void onLoadFailed_notifiesRequestCoordinator_beforeCallingRequestListeners() { AtomicBoolean isRequestCoordinatorVerified = new AtomicBoolean(); SingleRequest request = builder .setTarget(new DoNothingTarget()) .addRequestListener( new RequestListener<>() { @Override public boolean onLoadFailed( @Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { verify(builder.requestCoordinator).onRequestFailed(target.getRequest()); isRequestCoordinatorVerified.set(true); return false; } @Override public boolean onResourceReady( @NonNull List resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { return false; } }) .build(); builder.target.setRequest(request); request.onLoadFailed(new GlideException("test")); assertThat(isRequestCoordinatorVerified.get()).isTrue(); } // We don't need to clear a resource since we're not using it to being with. private static final class DoNothingTarget extends CustomTarget { @Override public void onResourceReady( @NonNull List resource, @Nullable Transition transition) {} @Override public void onLoadCleared(@Nullable Drawable placeholder) {} } @Test public void testRequestListenerIsCalledWithNotIsFirstRequestIfRequestCoordinatorParentReturnsResourceSet() { SingleRequest request = builder.addRequestListener(listener1).build(); RequestCoordinator rootRequestCoordinator = mock(RequestCoordinator.class); when(rootRequestCoordinator.isAnyResourceSet()).thenReturn(true); when(builder.requestCoordinator.isAnyResourceSet()).thenReturn(false); when(builder.requestCoordinator.getRoot()).thenReturn(rootRequestCoordinator); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ true); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(false)); verify(listener1) .onResourceReady( eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(false), eq(true)); } @Test public void testTargetIsCalledWithAnimationFromFactory() { SingleRequest request = builder.build(); Transition transition = mockTransition(); when(builder.transitionFactory.build(any(DataSource.class), anyBoolean())) .thenReturn(transition); request.onResourceReady( builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false); verify(builder.target).onResourceReady(eq(builder.result), eq(transition)); } @Test public void testCallsGetSizeIfOverrideWidthIsLessThanZero() { SingleRequest request = builder.setOverrideWidth(-1).setOverrideHeight(100).build(); request.begin(); verify(builder.target).getSize(any(SizeReadyCallback.class)); } @Test public void testCallsGetSizeIfOverrideHeightIsLessThanZero() { SingleRequest request = builder.setOverrideWidth(100).setOverrideHeight(-1).build(); request.begin(); verify(builder.target).getSize(any(SizeReadyCallback.class)); } @Test public void testDoesNotCallGetSizeIfOverrideWidthAndHeightAreSet() { SingleRequest request = builder.setOverrideWidth(100).setOverrideHeight(100).build(); request.begin(); verify(builder.target, never()).getSize(any(SizeReadyCallback.class)); } @Test public void testCallsEngineWithOverrideWidthAndHeightIfSet() { SingleRequest request = builder.setOverrideWidth(1).setOverrideHeight(2).build(); request.begin(); verify(builder.engine) .load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), anyInt(), anyInt(), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), anyBoolean(), /* useAnimationPool= */ anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor()); } @Test public void testDoesNotSetErrorDrawableIfRequestCoordinatorDoesntAllowIt() { SingleRequest request = builder.setErrorDrawable(new ColorDrawable(Color.RED)).build(); when(builder.requestCoordinator.canNotifyStatusChanged(any(Request.class))).thenReturn(false); request.onLoadFailed(new GlideException("test")); verify(builder.target, never()).onLoadFailed(any(Drawable.class)); } @Test public void testCanReRunClearedRequests() { doAnswer(new CallSizeReady(100, 100)) .when(builder.target) .getSize(any(SizeReadyCallback.class)); when(builder.engine.load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), eq(100), eq(100), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), anyBoolean(), /* useAnimationPool= */ anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor())) .thenAnswer(new CallResourceCallback(builder.resource)); SingleRequest request = builder.build(); request.begin(); request.clear(); request.begin(); verify(builder.target, times(2)).onResourceReady(eq(builder.result), anyTransition()); } @Test public void testResourceOnlyReceivesOneGetOnResourceReady() { SingleRequest request = builder.build(); request.onResourceReady( builder.resource, DataSource.LOCAL, /* isLoadedFromAlternateCacheKey= */ false); verify(builder.resource, times(1)).get(); } @Test public void testDoesNotStartALoadIfOnSizeReadyIsCalledAfterClear() { SingleRequest request = builder.build(); request.clear(); request.onSizeReady(100, 100); verify(builder.engine, never()) .load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), anyInt(), anyInt(), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), anyBoolean(), /* useAnimationPool= */ anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor()); } @Test public void testCallsSourceUnlimitedExecutorEngineIfOptionsIsSet() { doAnswer(new CallSizeReady(100, 100)) .when(builder.target) .getSize(any(SizeReadyCallback.class)); SingleRequest request = builder.setUseUnlimitedSourceGeneratorsPool(true).build(); request.begin(); verify(builder.engine) .load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), anyInt(), anyInt(), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), eq(true), /* useAnimationPool= */ anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor()); } @Test public void testCallsSourceExecutorEngineIfOptionsIsSet() { doAnswer(new CallSizeReady(100, 100)) .when(builder.target) .getSize(any(SizeReadyCallback.class)); SingleRequest request = builder.setUseUnlimitedSourceGeneratorsPool(false).build(); request.begin(); verify(builder.engine) .load( eq(builder.glideContext), eq(builder.model), eq(builder.signature), anyInt(), anyInt(), eq(Object.class), eq(List.class), any(Priority.class), any(DiskCacheStrategy.class), eq(builder.transformations), anyBoolean(), anyBoolean(), any(Options.class), anyBoolean(), eq(false), /* useAnimationPool= */ anyBoolean(), anyBoolean(), any(ResourceCallback.class), anyExecutor()); } @Test // Varargs @SuppressWarnings("unchecked") public void testIsEquivalentTo() { EquivalenceTester tester = EquivalenceTester.of( new Equivalence() { @Override protected boolean doEquivalent( @NonNull SingleRequestBuilder a, @NonNull SingleRequestBuilder b) { return a.build().isEquivalentTo(b.build()) && b.build().isEquivalentTo(a.build()); } @Override protected int doHash(@NonNull SingleRequestBuilder listSingleRequest) { return 0; } }); tester .addEquivalenceGroup( // Non-null request listeners are treated as equivalent, even if they're not equal. new SingleRequestBuilder().addRequestListener(listener1), new SingleRequestBuilder().addRequestListener(listener2)) .addEquivalenceGroup( new SingleRequestBuilder().setOverrideHeight(500), new SingleRequestBuilder().setOverrideHeight(500)) .addEquivalenceGroup( new SingleRequestBuilder().setOverrideWidth(500), new SingleRequestBuilder().setOverrideWidth(500)) .addEquivalenceGroup( new SingleRequestBuilder().setModel(12345), new SingleRequestBuilder().setModel(12345)) .addEquivalenceGroup( new SingleRequestBuilder().setModel(null), new SingleRequestBuilder().setModel(null)) .addEquivalenceGroup( new SingleRequestBuilder().setPriority(Priority.LOW), new SingleRequestBuilder().setPriority(Priority.LOW)) .test(); } static final class SingleRequestBuilder { private Engine engine = mock(Engine.class); private Number model = 123456; @SuppressWarnings("unchecked") private Target target = mock(Target.class); private Resource resource = mockResource(); private RequestCoordinator requestCoordinator = mock(RequestCoordinator.class); private Drawable placeholderDrawable = null; private Drawable errorDrawable = null; private Drawable fallbackDrawable = null; @SuppressWarnings("unchecked") private List> requestListeners = new ArrayList<>(); @SuppressWarnings("unchecked") private final TransitionFactory transitionFactory = mock(TransitionFactory.class); private int overrideWidth = -1; private int overrideHeight = -1; private List result = new ArrayList<>(); private final GlideContext glideContext = mock(GlideContext.class); private final Key signature = new ObjectKey(12345); private Priority priority = Priority.HIGH; private boolean useUnlimitedSourceGeneratorsPool = false; private final Class transcodeClass = List.class; private final Map, Transformation> transformations = new HashMap<>(); SingleRequestBuilder() { when(glideContext.getExperiments()).thenReturn(mock(GlideExperiments.class)); when(requestCoordinator.getRoot()).thenReturn(requestCoordinator); when(requestCoordinator.canSetImage(any(Request.class))).thenReturn(true); when(requestCoordinator.canNotifyCleared(any(Request.class))).thenReturn(true); when(requestCoordinator.canNotifyStatusChanged(any(Request.class))).thenReturn(true); when(resource.get()).thenReturn(result); } SingleRequestBuilder setEngine(Engine engine) { this.engine = engine; return this; } SingleRequestBuilder setModel(Number model) { this.model = model; return this; } SingleRequestBuilder setTarget(Target target) { this.target = target; return this; } SingleRequestBuilder setResource(Resource resource) { this.resource = resource; return this; } SingleRequestBuilder setRequestCoordinator(RequestCoordinator requestCoordinator) { this.requestCoordinator = requestCoordinator; return this; } SingleRequestBuilder setPlaceholderDrawable(Drawable placeholderDrawable) { this.placeholderDrawable = placeholderDrawable; return this; } SingleRequestBuilder setErrorDrawable(Drawable errorDrawable) { this.errorDrawable = errorDrawable; return this; } SingleRequestBuilder setFallbackDrawable(Drawable fallbackDrawable) { this.fallbackDrawable = fallbackDrawable; return this; } SingleRequestBuilder addRequestListener(RequestListener requestListener) { this.requestListeners.add(requestListener); return this; } SingleRequestBuilder setOverrideWidth(int overrideWidth) { this.overrideWidth = overrideWidth; return this; } SingleRequestBuilder setOverrideHeight(int overrideHeight) { this.overrideHeight = overrideHeight; return this; } SingleRequestBuilder setResult(List result) { this.result = result; return this; } SingleRequestBuilder setPriority(Priority priority) { this.priority = priority; return this; } SingleRequestBuilder setUseUnlimitedSourceGeneratorsPool( boolean useUnlimitedSourceGeneratorsPool) { this.useUnlimitedSourceGeneratorsPool = useUnlimitedSourceGeneratorsPool; return this; } SingleRequest build() { RequestOptions requestOptions = new RequestOptions() .error(errorDrawable) .placeholder(placeholderDrawable) .fallback(fallbackDrawable) .override(overrideWidth, overrideHeight) .priority(priority) .signature(signature) .useUnlimitedSourceGeneratorsPool(useUnlimitedSourceGeneratorsPool); return SingleRequest.obtain( /* context= */ glideContext, /* glideContext= */ glideContext, /* requestLock= */ new Object(), model, transcodeClass, requestOptions, overrideWidth, overrideHeight, priority, target, /* targetListener= */ null, requestListeners, requestCoordinator, engine, transitionFactory, Executors.directExecutor()); } } private static Drawable anyDrawableOrNull() { return any(); } // TODO do we want to move these to Util? @SuppressWarnings("unchecked") private static Transition mockTransition() { return mock(Transition.class); } @SuppressWarnings("unchecked") private static Target isAListTarget() { return isA(Target.class); } private static GlideException isAGlideException() { return isA(GlideException.class); } @SuppressWarnings("unchecked") private static Transition anyTransition() { return any(); } private static Executor anyExecutor() { return any(Executor.class); } private static class CallResourceCallback implements Answer { private final Resource resource; CallResourceCallback(Resource resource) { this.resource = resource; } @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { ResourceCallback cb = (ResourceCallback) invocationOnMock.getArguments()[invocationOnMock.getArguments().length - 2]; cb.onResourceReady(resource, DataSource.REMOTE, /* isLoadedFromAlternateCacheKey= */ false); return null; } } private static class CallSizeReady implements Answer { private final int width; private final int height; CallSizeReady(int width, int height) { this.width = width; this.height = height; } @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { SizeReadyCallback cb = (SizeReadyCallback) invocationOnMock.getArguments()[0]; cb.onSizeReady(width, height); return null; } } private static class MockTarget implements Target { private Drawable currentPlaceholder; @Override public void onLoadCleared(@Nullable Drawable placeholder) { currentPlaceholder = placeholder; } @Override public void onLoadStarted(@Nullable Drawable placeholder) { currentPlaceholder = placeholder; } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { currentPlaceholder = errorDrawable; } @Override public void onResourceReady( @NonNull List resource, @Nullable Transition transition) { currentPlaceholder = null; } @Override public void getSize(@NonNull SizeReadyCallback cb) {} @Override public void removeCallback(@NonNull SizeReadyCallback cb) { // Do nothing. } @Override public void setRequest(@Nullable Request request) {} @Nullable @Override public Request getRequest() { return null; } @Override public void onStart() {} @Override public void onStop() {} @Override public void onDestroy() {} } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/ThumbnailRequestCoordinatorTest.java ================================================ package com.bumptech.glide.request; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @RunWith(JUnit4.class) public class ThumbnailRequestCoordinatorTest { @Mock private Request full; @Mock private Request thumb; @Mock private RequestCoordinator parent; private ThumbnailRequestCoordinator coordinator; @Before public void setUp() { MockitoAnnotations.initMocks(this); coordinator = newCoordinator(); coordinator.setRequests(full, thumb); } @Test public void testIsRunningIsFalseIfNeitherRequestIsRunning() { assertFalse(coordinator.isRunning()); } @Test public void isRunning_withThumbAndFullRunning_isTrue() { coordinator.begin(); assertTrue(coordinator.isRunning()); } @Test public void isRunning_withFullRunning_isTrue() { coordinator.begin(); coordinator.onRequestSuccess(thumb); assertTrue(coordinator.isRunning()); } @Test public void isRunning_withThumbRunning_fullComplete_isFalse() { coordinator.begin(); coordinator.onRequestSuccess(full); assertFalse(coordinator.isRunning()); } @Test public void testStartsFullOnRunIfNotRunning() { coordinator.begin(); verify(full).begin(); } @Test public void testStartsThumbOnRunIfNotRunning() { coordinator.begin(); verify(thumb).begin(); } @Test public void testDoesNotStartFullOnRunIfRunning() { coordinator.begin(); coordinator.begin(); verify(full, times(1)).begin(); } @Test public void testDoesNotStartThumbOnRunIfRunning() { coordinator.begin(); coordinator.begin(); verify(thumb, times(1)).begin(); } @Test public void begin_whenFullIsComplete_startsFull() { coordinator.onRequestSuccess(full); coordinator.begin(); verify(full).begin(); } @Test public void begin_whenFullIsComplete_doesNotBeginThumb() { coordinator.onRequestSuccess(full); coordinator.begin(); verify(thumb, never()).begin(); } @Test public void testDoesNotStartFullIfClearedByThumb() { doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { coordinator.clear(); return null; } }) .when(thumb) .begin(); coordinator.begin(); verify(full, never()).begin(); } @Test public void testCallsClearOnRequestsWhenCleared() { coordinator.clear(); InOrder order = inOrder(thumb, full); order.verify(thumb).clear(); order.verify(full).clear(); } @Test public void pause_pausesThumbAndFullInOrder() { coordinator.begin(); coordinator.pause(); InOrder order = inOrder(thumb, full); order.verify(thumb).pause(); order.verify(full).pause(); } @Test public void testCanSetImageReturnsTrueForFullRequestIfCoordinatorIsNull() { coordinator = newCoordinator(); coordinator.setRequests(full, thumb); assertTrue(coordinator.canSetImage(full)); } @Test public void testCanSetImageReturnsTrueForFullRequestIfParentAllowsSetImage() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canSetImage(eq(coordinator))).thenReturn(true); assertTrue(coordinator.canSetImage(full)); } @Test public void testCanSetImageReturnsFalseForFullRequestIfParentDoesNotAllowSetImage() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canSetImage(eq(coordinator))).thenReturn(false); assertFalse(coordinator.canSetImage(full)); } @Test public void canSetImage_forThumb_withNullParent_fullNotComplete_returnsTrue() { assertTrue(coordinator.canSetImage(thumb)); } @Test public void canSetImage_forThumb_withNullParent_fullComplete_returnsFalse() { coordinator.onRequestSuccess(full); assertFalse(coordinator.canSetImage(thumb)); } @Test public void canSetImage_forThumb_whenDisallowedByParent_fullNotComplete_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canSetImage(eq(coordinator))).thenReturn(false); assertFalse(coordinator.canSetImage(thumb)); } @Test public void canSetImage_forThumb_whenDisallowedByParent_fullComplete_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canSetImage(eq(coordinator))).thenReturn(false); coordinator.onRequestSuccess(full); assertFalse(coordinator.canSetImage(thumb)); } @Test public void testCanNotifyStatusChangedIfFullAndNoRequestsAreComplete() { assertTrue(coordinator.canNotifyStatusChanged(full)); } @Test public void testCanNotNotifyStatusChangedIfThumb() { assertFalse(coordinator.canNotifyStatusChanged(thumb)); } @Test public void canNotNotifyStatusChanged_forFull_whenFullComplete_isFalse() { when(full.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(full); assertFalse(coordinator.canNotifyStatusChanged(full)); } @Test public void canNotNotifyStatusChanged_forFull_whenIfThumbComplete_isFalse() { when(thumb.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(thumb); assertFalse(coordinator.canNotifyStatusChanged(full)); } @Test public void testCanNotNotifyStatusChangedIfParentHasResourceSet() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.isAnyResourceSet()).thenReturn(true); assertFalse(coordinator.canNotifyStatusChanged(full)); } @Test public void testCanNotifyStatusChangedIfParentAllowsNotify() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canNotifyStatusChanged(eq(coordinator))).thenReturn(true); assertTrue(coordinator.canNotifyStatusChanged(full)); } @Test public void testCanNotNotifyStatusChangedIfParentDoesNotAllowNotify() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canNotifyStatusChanged(eq(coordinator))).thenReturn(false); assertFalse(coordinator.canNotifyStatusChanged(full)); } @Test public void isAnyResourceSet_withIncompleteThumbAndFull_isFalse() { assertFalse(coordinator.isAnyResourceSet()); } @Test public void isAnyResourceSet_withCompleteFull_isTrue() { when(full.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(full); assertTrue(coordinator.isAnyResourceSet()); } @Test public void isAnyResourceSet_withCompleteThumb_isTrue() { when(thumb.isAnyResourceSet()).thenReturn(true); coordinator.onRequestSuccess(thumb); assertTrue(coordinator.isAnyResourceSet()); } @Test public void isAnyResourceSet_withParentResourceSet_isFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.isAnyResourceSet()).thenReturn(true); assertThat(coordinator.isAnyResourceSet()).isFalse(); } @Test public void testIsNotCompleteIfNeitherRequestIsComplete() { assertFalse(coordinator.isComplete()); } @Test public void isComplete_withFullComplete_isTrue() { coordinator.onRequestSuccess(full); assertTrue(coordinator.isComplete()); } @Test public void isComplete_withOnlyThumbComplete_returnsFalse() { coordinator.onRequestSuccess(thumb); assertThat(coordinator.isComplete()).isFalse(); } @Test public void testClearsThumbRequestOnFullRequestComplete_withNullParent() { coordinator.onRequestSuccess(full); verify(thumb).clear(); } @Test public void testNotifiesParentOnFullRequestComplete_withNonNullParent() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); coordinator.onRequestSuccess(full); verify(parent).onRequestSuccess(eq(coordinator)); } @Test public void testClearsThumbRequestOnFullRequestComplete_withNonNullParent() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); coordinator.onRequestSuccess(full); verify(thumb).clear(); } @Test public void testDoesNotClearThumbOnThumbRequestComplete() { coordinator.onRequestSuccess(thumb); verify(thumb, never()).clear(); } @Test public void testDoesNotClearThumbOnFullComplete_whenThumbIsComplete() { coordinator.onRequestSuccess(thumb); coordinator.onRequestSuccess(full); verify(thumb, never()).clear(); } @Test public void testDoesNotNotifyParentOnThumbRequestComplete() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); coordinator.onRequestSuccess(thumb); verify(parent, never()).onRequestSuccess(any(Request.class)); } @Test public void canNotifyCleared_withThumbRequest_returnsFalse() { assertThat(coordinator.canNotifyCleared(thumb)).isFalse(); } @Test public void canNotifyCleared_withFullRequest_andNullParent_returnsTrue() { assertThat(coordinator.canNotifyCleared(full)).isTrue(); } @Test public void canNotifyCleared_withFullRequest_nonNullParent_parentCanClear_returnsTrue() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canNotifyCleared(coordinator)).thenReturn(true); assertThat(coordinator.canNotifyCleared(full)).isTrue(); } @Test public void canNotifyCleared_withFullRequest_nonNullParent_parentCanNotClear_returnsFalse() { coordinator = newCoordinator(parent); coordinator.setRequests(full, thumb); when(parent.canNotifyCleared(coordinator)).thenReturn(false); assertThat(coordinator.canNotifyCleared(full)).isFalse(); } @Test public void canNotifyCleared_withFullRequest_afterPause_returnsFalse() { coordinator.pause(); assertThat(coordinator.canNotifyCleared(full)).isFalse(); } @Test public void canNotifyCleared_withFullRequest_afterPauseAndResume_returnsTrue() { coordinator.pause(); coordinator.begin(); assertThat(coordinator.canNotifyCleared(full)).isTrue(); } @Test public void canNotifyCleared_withFullRequest_afterPauseAndClear_returnsTrue() { coordinator.pause(); coordinator.clear(); assertThat(coordinator.canNotifyCleared(full)).isTrue(); } @Test public void testIsEquivalentTo() { ThumbnailRequestCoordinator first = newCoordinator(); when(full.isEquivalentTo(full)).thenReturn(true); when(thumb.isEquivalentTo(thumb)).thenReturn(true); first.setRequests(full, thumb); assertTrue(first.isEquivalentTo(first)); ThumbnailRequestCoordinator second = newCoordinator(); second.setRequests(full, full); assertTrue(second.isEquivalentTo(second)); assertFalse(second.isEquivalentTo(first)); assertFalse(first.isEquivalentTo(second)); ThumbnailRequestCoordinator third = newCoordinator(); third.setRequests(thumb, thumb); assertTrue(third.isEquivalentTo(third)); assertFalse(third.isEquivalentTo(first)); assertFalse(first.isEquivalentTo(third)); } private static ThumbnailRequestCoordinator newCoordinator() { return newCoordinator(/* parent= */ null); } private static ThumbnailRequestCoordinator newCoordinator(RequestCoordinator parent) { return new ThumbnailRequestCoordinator(/* requestLock= */ new Object(), parent); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/AppWidgetTargetTest.java ================================================ package com.bumptech.glide.request.target; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.graphics.Bitmap; import android.widget.RemoteViews; import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowAppWidgetManager; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK, shadows = AppWidgetTargetTest.UpdateShadowAppWidgetManager.class) public class AppWidgetTargetTest { private UpdateShadowAppWidgetManager shadowManager; private RemoteViews views; private int viewId; @Before public void setUp() { shadowManager = Shadow.extract(AppWidgetManager.getInstance(ApplicationProvider.getApplicationContext())); viewId = 1234; views = mock(RemoteViews.class); } @Test public void testSetsBitmapOnRemoteViewsWithViewIdWhenCreatedWithComponentName() { ComponentName componentName = mock(ComponentName.class); AppWidgetTarget target = new AppWidgetTarget( ApplicationProvider.getApplicationContext(), viewId, views, componentName); Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); target.onResourceReady(bitmap, null /*glideAnimation*/); verify(views).setImageViewBitmap(eq(viewId), eq(bitmap)); } @Test public void testUpdatesAppWidgetWhenCreatedWithComponentName() { ComponentName componentName = mock(ComponentName.class); AppWidgetTarget target = new AppWidgetTarget( ApplicationProvider.getApplicationContext(), viewId, views, componentName); target.onResourceReady( Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), null /*glideAnimation*/ ); assertEquals(componentName, shadowManager.updatedComponentName); assertEquals(views, shadowManager.updatedRemoteViews); } @Test public void testSetsBitmapOnRemoteViewsWithViewIdWhenCreatedWithWidgetIds() { int[] widgetIds = new int[] {1}; AppWidgetTarget target = new AppWidgetTarget(ApplicationProvider.getApplicationContext(), viewId, views, widgetIds); Bitmap bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565); target.onResourceReady(bitmap, null /*glideAnimation*/); verify(views).setImageViewBitmap(eq(viewId), eq(bitmap)); } @Test public void testUpdatesAppWidgetWhenCreatedWithWidgetIds() { int[] widgetIds = new int[] {1}; AppWidgetTarget target = new AppWidgetTarget(ApplicationProvider.getApplicationContext(), viewId, views, widgetIds); target.onResourceReady( Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888), null /*glideAnimation*/ ); assertThat(widgetIds).isEqualTo(shadowManager.updatedWidgetIds); assertEquals(views, shadowManager.updatedRemoteViews); } @Test(expected = NullPointerException.class) public void testThrowsWhenGivenNullContextWithWidgetIds() { new AppWidgetTarget(null /*context*/, 1234 /*viewId*/, views, 1 /*widgetIds*/); } @Test(expected = NullPointerException.class) public void testThrowsWhenGivenNullContextWithComponentName() { new AppWidgetTarget(null /*context*/, 1234 /*viewId*/, views, mock(ComponentName.class)); } @Test(expected = NullPointerException.class) public void testThrowsWhenGivenNullRemoteViewsWithWidgetIds() { new AppWidgetTarget( ApplicationProvider.getApplicationContext(), viewId, null /*remoteViews*/, 1 /*widgetIds*/); } @Test(expected = NullPointerException.class) public void testThrowsWhenGivenNullRemoteViewsWithComponentName() { new AppWidgetTarget( ApplicationProvider.getApplicationContext(), viewId, null /*remoteViews*/, mock(ComponentName.class)); } @Test(expected = NullPointerException.class) public void testThrowsWhenGivenNullWidgetIds() { new AppWidgetTarget( ApplicationProvider.getApplicationContext(), viewId, views, (int[]) null /*widgetIds*/); } @Test(expected = IllegalArgumentException.class) public void testThrowsWhenGivenEmptyWidgetIds() { new AppWidgetTarget(ApplicationProvider.getApplicationContext(), viewId, views); } @Test(expected = NullPointerException.class) public void testThrowsWhenGivenNullComponentName() { new AppWidgetTarget( ApplicationProvider.getApplicationContext(), viewId, views, (ComponentName) null); } @Implements(AppWidgetManager.class) public static class UpdateShadowAppWidgetManager extends ShadowAppWidgetManager { int[] updatedWidgetIds; RemoteViews updatedRemoteViews; ComponentName updatedComponentName; @Implementation @Override public void updateAppWidget(int[] appWidgetIds, RemoteViews views) { updatedWidgetIds = appWidgetIds; updatedRemoteViews = views; } @Implementation public void updateAppWidget(ComponentName componentName, RemoteViews views) { updatedComponentName = componentName; updatedRemoteViews = views; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/BitmapImageViewTargetTest.java ================================================ package com.bumptech.glide.request.target; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class BitmapImageViewTargetTest { private ImageView view; private BitmapImageViewTarget target; @Before public void setUp() { view = new ImageView(ApplicationProvider.getApplicationContext()); target = new BitmapImageViewTarget(view); } @Test public void testSetsBitmapOnViewInSetResource() { Bitmap bitmap = Bitmap.createBitmap(100, 75, Bitmap.Config.RGB_565); target.setResource(bitmap); assertEquals(bitmap, ((BitmapDrawable) view.getDrawable()).getBitmap()); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/ImageViewTargetFactoryTest.java ================================================ package com.bumptech.glide.request.target; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ImageViewTargetFactoryTest { private ImageViewTargetFactory factory; private ImageView view; @Before public void setUp() { factory = new ImageViewTargetFactory(); view = new ImageView(ApplicationProvider.getApplicationContext()); } @Test public void testReturnsTargetForBitmaps() { Bitmap bitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); Target target = factory.buildTarget(view, Bitmap.class); target.onResourceReady(bitmap, null); assertThat(target).isInstanceOf(BitmapImageViewTarget.class); } @Test public void testReturnsTargetForBitmapDrawables() { BitmapDrawable drawable = new BitmapDrawable( ApplicationProvider.getApplicationContext().getResources(), Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444)); Target target = factory.buildTarget(view, BitmapDrawable.class); target.onResourceReady(drawable, null); assertThat(target).isInstanceOf(DrawableImageViewTarget.class); } @Test public void testReturnsTargetForDrawables() { Target target = factory.buildTarget(view, Drawable.class); target.onResourceReady(new ColorDrawable(Color.RED), null); assertThat(target).isInstanceOf(DrawableImageViewTarget.class); } @Test(expected = IllegalArgumentException.class) public void testThrowsForUnknownType() { factory.buildTarget(view, Object.class); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/ImageViewTargetTest.java ================================================ package com.bumptech.glide.request.target; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Color; import android.graphics.drawable.Animatable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.request.transition.Transition; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ImageViewTargetTest { @Mock private AnimatedDrawable animatedDrawable; private ImageView view; private TestTarget target; private ColorDrawable drawable; @Before public void setUp() { MockitoAnnotations.initMocks(this); view = new ImageView(ApplicationProvider.getApplicationContext()); target = new TestTarget(view); drawable = new ColorDrawable(Color.RED); } @Test public void testReturnsCurrentDrawable() { view.setImageDrawable(drawable); assertEquals(drawable, target.getCurrentDrawable()); } @Test public void testSetsDrawableSetsDrawableOnView() { target.setDrawable(drawable); assertEquals(drawable, view.getDrawable()); } @Test public void testSetsDrawableOnLoadStarted() { target.onLoadStarted(drawable); assertEquals(drawable, view.getDrawable()); } @Test public void testSetDrawableOnLoadFailed() { target.onLoadFailed(drawable); assertEquals(drawable, view.getDrawable()); } @Test public void testSetsDrawableOnLoadCleared() { target.onLoadCleared(drawable); assertEquals(drawable, view.getDrawable()); } @Test public void testSetsDrawableOnViewInOnResourceReadyWhenAnimationReturnsFalse() { @SuppressWarnings("unchecked") Transition animation = mock(Transition.class); when(animation.transition(any(Drawable.class), eq(target))).thenReturn(false); Drawable resource = new ColorDrawable(Color.GRAY); target.onResourceReady(resource, animation); assertEquals(resource, target.resource); } @Test public void testDoesNotSetDrawableOnViewInOnResourceReadyWhenAnimationReturnsTrue() { Drawable resource = new ColorDrawable(Color.RED); @SuppressWarnings("unchecked") Transition animation = mock(Transition.class); when(animation.transition(eq(resource), eq(target))).thenReturn(true); target.onResourceReady(resource, animation); assertNull(target.resource); } @Test public void testProvidesCurrentPlaceholderToAnimationIfPresent() { Drawable placeholder = new ColorDrawable(Color.BLACK); view.setImageDrawable(placeholder); @SuppressWarnings("unchecked") Transition animation = mock(Transition.class); target.onResourceReady(new ColorDrawable(Color.GREEN), animation); ArgumentCaptor drawableCaptor = ArgumentCaptor.forClass(Drawable.class); verify(animation).transition(drawableCaptor.capture(), eq(target)); assertThat(((ColorDrawable) drawableCaptor.getValue()).getColor()).isEqualTo(Color.GREEN); } @Test public void onResourceReady_withAnimatableResource_startsAnimatableAfterSetResource() { AnimatedDrawable drawable = mock(AnimatedDrawable.class); ImageView view = mock(ImageView.class); target = new TestTarget(view); target.onResourceReady(drawable, /* transition= */ null); InOrder order = inOrder(view, drawable); order.verify(view).setImageDrawable(drawable); order.verify(drawable).start(); } @Test public void onLoadCleared_withAnimatableDrawable_stopsDrawable() { target.onResourceReady(animatedDrawable, /* transition= */ null); verify(animatedDrawable).start(); verify(animatedDrawable, never()).stop(); target.onLoadCleared(/* placeholder= */ null); verify(animatedDrawable).stop(); } private abstract static class AnimatedDrawable extends Drawable implements Animatable { // Intentionally empty. } private static final class TestTarget extends ImageViewTarget { public Drawable resource; TestTarget(ImageView view) { super(view); } @Override protected void setResource(Drawable resource) { this.resource = resource; view.setImageDrawable(resource); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/NotificationTargetTest.java ================================================ package com.bumptech.glide.request.target; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; import android.graphics.Bitmap; import android.widget.RemoteViews; import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowNotificationManager; @RunWith(RobolectricTestRunner.class) @Config( sdk = ROBOLECTRIC_SDK, shadows = NotificationTargetTest.UpdateShadowNotificationManager.class) public class NotificationTargetTest { private UpdateShadowNotificationManager shadowManager; private RemoteViews remoteViews; private int viewId; private Notification notification; private int notificationId; private String notificationTag; private NotificationTarget target; @Before public void setUp() { NotificationManager notificationManager = (NotificationManager) ApplicationProvider.getApplicationContext() .getSystemService(Context.NOTIFICATION_SERVICE); shadowManager = Shadow.extract(notificationManager); remoteViews = mock(RemoteViews.class); viewId = 123; notification = mock(Notification.class); notificationId = 456; notificationTag = "tag"; target = new NotificationTarget( ApplicationProvider.getApplicationContext(), 100 /*width*/, 100 /*height*/, viewId, remoteViews, notification, notificationId, notificationTag); } @Test public void testSetsBitmapOnRemoteViewsWithGivenImageIdOnResourceReady() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); target.onResourceReady(bitmap, null /*glideAnimation*/); verify(remoteViews).setImageViewBitmap(eq(viewId), eq(bitmap)); } @Test public void updatesNotificationManagerWithNotificationIdAndNotificationOnResourceReady() { target.onResourceReady( Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), null /*glideAnimation*/ ); assertEquals(notificationId, shadowManager.updatedNotificationId); assertEquals(notificationTag, shadowManager.updatedNotificationTag); assertEquals(notification, shadowManager.updatedNotification); } @Test(expected = NullPointerException.class) public void testThrowsIfContextIsNull() { new NotificationTarget( null /*context*/, 100 /*width*/, 100 /*height*/, 123 /*viewId*/, mock(RemoteViews.class), mock(Notification.class), 456 /*notificationId*/, "tag" /*notificationTag*/); } @Test(expected = NullPointerException.class) public void testThrowsIfNotificationIsNull() { new NotificationTarget( ApplicationProvider.getApplicationContext(), 100 /*width*/, 100 /*height*/, 123 /*viewId*/, mock(RemoteViews.class), null /*notification*/, 456 /*notificationId*/, "tag" /*notificationTag*/); } @Test(expected = NullPointerException.class) public void testThrowsIfRemoteViewsIsNull() { new NotificationTarget( ApplicationProvider.getApplicationContext(), 100 /*width*/, 100 /*height*/, 123 /*viewId*/, null /*remoteViews*/, mock(Notification.class), 456 /*notificationId*/, "tag" /*notificationTag*/); } @Implements(NotificationManager.class) public static class UpdateShadowNotificationManager extends ShadowNotificationManager { int updatedNotificationId; String updatedNotificationTag; Notification updatedNotification; @Implementation @Override public void notify(String notificationTag, int notificationId, Notification notification) { updatedNotificationTag = notificationTag; updatedNotificationId = notificationId; updatedNotification = notification; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/PreloadTargetTest.java ================================================ package com.bumptech.glide.request.target; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.os.Looper; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.Request; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class PreloadTargetTest { @Mock private RequestManager requestManager; @Before public void setUp() { MockitoAnnotations.initMocks(this); shadowOf(Looper.getMainLooper()).pause(); } @Test public void testCallsSizeReadyWithGivenDimensions() { int width = 1234; int height = 456; PreloadTarget target = PreloadTarget.obtain(requestManager, width, height); SizeReadyCallback cb = mock(SizeReadyCallback.class); target.getSize(cb); verify(cb).onSizeReady(eq(width), eq(height)); } // This isn't really supposed to happen, but just to double check... @Test public void onResourceReady_withNullRequest_doesNotClearTarget() { PreloadTarget target = PreloadTarget.obtain(requestManager, 100, 100); target.setRequest(null); callOnResourceReadyAndRunUiRunnables(target); verify(requestManager, never()).clear(target); } @Test public void onResourceReady_withNotYetCompleteRequest_doesNotClearTarget() { Request request = mock(Request.class); when(request.isComplete()).thenReturn(false); PreloadTarget target = PreloadTarget.obtain(requestManager, 100, 100); target.setRequest(request); callOnResourceReadyAndRunUiRunnables(target); verify(requestManager, never()).clear(target); } @Test public void onResourceReady_withCompleteRequest_postsToClearTarget() { Request request = mock(Request.class); when(request.isComplete()).thenReturn(true); PreloadTarget target = PreloadTarget.obtain(requestManager, 100, 100); target.setRequest(request); callOnResourceReadyAndRunUiRunnables(target); verify(requestManager).clear(target); } @Test public void onResourceReady_withCompleteRequest_doesNotImmediatelyClearTarget() { Request request = mock(Request.class); when(request.isComplete()).thenReturn(true); PreloadTarget target = PreloadTarget.obtain(requestManager, 100, 100); target.setRequest(request); target.onResourceReady(new Object(), /* transition= */ null); verify(requestManager, never()).clear(target); } private void callOnResourceReadyAndRunUiRunnables(Target target) { target.onResourceReady(new Object(), /* transition= */ null); shadowOf(Looper.getMainLooper()).idle(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/SimpleTargetTest.java ================================================ package com.bumptech.glide.request.target; import static org.mockito.Mockito.mock; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.request.transition.Transition; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class SimpleTargetTest { @Test(expected = IllegalArgumentException.class) public void testThrowsOnGetSizeIfGivenWidthIsLessThanZero() { getTarget(-1, 1).getSize(mock(SizeReadyCallback.class)); } @Test(expected = IllegalArgumentException.class) public void testThrowsOnGetSizeIfGivenWidthIsEqualToZero() { getTarget(0, 1).getSize(mock(SizeReadyCallback.class)); } @Test(expected = IllegalArgumentException.class) public void testThrowsOnGetSizeIfGivenHeightIsLessThanZero() { getTarget(1, -1).getSize(mock(SizeReadyCallback.class)); } @Test(expected = IllegalArgumentException.class) public void testThrowsOnGetSizeIfGivenHeightIsEqualToZero() { getTarget(1, 0).getSize(mock(SizeReadyCallback.class)); } @Test public void testCanBeConstructedWithoutDimensions() { new SimpleTarget() { @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) { // Do nothing. } }; } @Test public void testConstructorDoesNotThrowWithSizeOriginal() { getTarget(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); } @Test public void testGetSizeDoesNotThrowWithSizeOriginal() { getTarget(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).getSize(mock(SizeReadyCallback.class)); } private SimpleTarget getTarget(int width, int height) { return new SimpleTarget(width, height) { @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) { // Do nothing. } }; } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/target/ViewTargetTest.java ================================================ package com.bumptech.glide.request.target; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.Display; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnPreDrawListener; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.tests.Util; import com.bumptech.glide.util.Preconditions; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowView; @RunWith(RobolectricTestRunner.class) @Config( sdk = ROBOLECTRIC_SDK, shadows = { ViewTargetTest.SizedShadowView.class, ViewTargetTest.PreDrawShadowViewTreeObserver.class }) public class ViewTargetTest { private View view; private ViewTarget target; private SizedShadowView shadowView; private PreDrawShadowViewTreeObserver shadowObserver; @Mock private SizeReadyCallback cb; @Mock private Request request; private int sdkVersion; private AttachStateTarget attachStateTarget; @Before public void setUp() { sdkVersion = Build.VERSION.SDK_INT; MockitoAnnotations.initMocks(this); view = new View(ApplicationProvider.getApplicationContext()); target = new TestViewTarget(view); attachStateTarget = new AttachStateTarget(view); shadowView = Shadow.extract(view); shadowObserver = Shadow.extract(view.getViewTreeObserver()); } @After public void tearDown() { Util.setSdkVersionInt(sdkVersion); ViewTarget.SizeDeterminer.maxDisplayLength = null; } @Test public void testReturnsWrappedView() { assertEquals(view, target.getView()); } @Test public void testReturnsNullFromGetRequestIfNoRequestSet() { assertNull(target.getRequest()); } @Test public void testCanSetAndRetrieveRequest() { target.setRequest(request); assertEquals(request, target.getRequest()); } @Test public void testRetrievesRequestFromPreviousTargetForView() { target.setRequest(request); ViewTarget second = new TestViewTarget(view); assertEquals(request, second.getRequest()); } @Test public void testSizeCallbackIsCalledSynchronouslyIfViewSizeSet() { int dimens = 333; shadowView.setWidth(dimens).setHeight(dimens).setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(eq(dimens), eq(dimens)); } @Test public void testSizeCallbackIsCalledSynchronouslyIfLayoutParamsConcreteSizeSet() { int dimens = 444; LayoutParams layoutParams = new LayoutParams(dimens, dimens); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(eq(dimens), eq(dimens)); } @Test public void getSize_withBothWrapContent_usesDisplayDimens() { LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); setDisplayDimens(200, 300); target.getSize(cb); verify(cb).onSizeReady(300, 300); } @Test public void getSize_withWrapContentWidthAndValidHeight_usesDisplayDimenAndValidHeight() { int height = 100; LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, height); view.setLayoutParams(params); shadowView.setIsLaidOut(true); setDisplayDimens(100, 200); target.getSize(cb); verify(cb).onSizeReady(200, height); } @Test public void getSize_withWrapContentHeightAndValidWidth_returnsWidthAndDisplayDimen() { int width = 100; LayoutParams params = new LayoutParams(width, LayoutParams.WRAP_CONTENT); view.setLayoutParams(params); shadowView.setIsLaidOut(true); setDisplayDimens(200, 100); target.getSize(cb); verify(cb).onSizeReady(width, 200); } @Test public void getSize_withWrapContentWidthAndMatchParentHeight_usesDisplayDimenWidthAndHeight() { LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); view.setLayoutParams(params); setDisplayDimens(500, 600); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int height = 32; shadowView.setHeight(height).setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(600, height); } @Test public void getSize_withMatchParentWidthAndWrapContentHeight_usesWidthAndDisplayDimenHeight() { LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); view.setLayoutParams(params); setDisplayDimens(300, 400); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int width = 32; shadowView.setWidth(width).setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(width, 400); } @Test public void testMatchParentWidthAndHeight() { LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); view.setLayoutParams(params); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); int width = 32; int height = 45; shadowView.setWidth(width).setHeight(height).setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParams() { target.getSize(cb); int width = 12; int height = 32; shadowView.setWidth(width).setHeight(height).setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testSizeCallbacksAreCalledInOrderPreDraw() { SizeReadyCallback[] cbs = new SizeReadyCallback[25]; for (int i = 0; i < cbs.length; i++) { cbs[i] = mock(SizeReadyCallback.class); target.getSize(cbs[i]); } int width = 100; int height = 111; shadowView.setWidth(width).setHeight(height).setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); InOrder order = inOrder((Object[]) cbs); for (SizeReadyCallback cb : cbs) { order.verify(cb).onSizeReady(eq(width), eq(height)); } } @Test public void testDoesNotNotifyCallbackTwiceIfAddedTwice() { target.getSize(cb); target.getSize(cb); view.setLayoutParams(new LayoutParams(100, 100)); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void testDoesNotAddMultipleListenersIfMultipleCallbacksAreAdded() { SizeReadyCallback cb1 = mock(SizeReadyCallback.class); SizeReadyCallback cb2 = mock(SizeReadyCallback.class); target.getSize(cb1); target.getSize(cb2); assertThat(shadowObserver.getPreDrawListeners()).hasSize(1); } @Test public void testDoesAddSecondListenerIfFirstListenerIsRemovedBeforeSecondRequest() { SizeReadyCallback cb1 = mock(SizeReadyCallback.class); target.getSize(cb1); view.setLayoutParams(new LayoutParams(100, 100)); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); assertThat(shadowObserver.getPreDrawListeners()).hasSize(0); SizeReadyCallback cb2 = mock(SizeReadyCallback.class); view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); target.getSize(cb2); view.setLayoutParams(new LayoutParams(100, 100)); shadowObserver.fireOnPreDrawListeners(); verify(cb2).onSizeReady(anyInt(), anyInt()); } @Test public void testSizeCallbackIsNotCalledPreDrawIfNoDimensSetOnPreDraw() { target.getSize(cb); shadowObserver.fireOnPreDrawListeners(); verify(cb, never()).onSizeReady(anyInt(), anyInt()); assertThat(shadowObserver.getPreDrawListeners()).hasSize(1); } @Test public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParamsButLayoutParamsSetLater() { target.getSize(cb); int width = 689; int height = 354; LayoutParams layoutParams = new LayoutParams(width, height); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test public void testCallbackIsNotCalledTwiceIfPreDrawFiresTwice() { target.getSize(cb); LayoutParams layoutParams = new LayoutParams(1234, 4123); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); shadowObserver.fireOnPreDrawListeners(); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void testCallbacksFromMultipleRequestsAreNotifiedOnPreDraw() { SizeReadyCallback firstCb = mock(SizeReadyCallback.class); SizeReadyCallback secondCb = mock(SizeReadyCallback.class); target.getSize(firstCb); target.getSize(secondCb); int width = 68; int height = 875; LayoutParams layoutParams = new LayoutParams(width, height); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.fireOnPreDrawListeners(); shadowObserver.fireOnPreDrawListeners(); verify(firstCb, times(1)).onSizeReady(eq(width), eq(height)); verify(secondCb, times(1)).onSizeReady(eq(width), eq(height)); } @Test public void testDoesNotThrowOnPreDrawIfViewTreeObserverIsDead() { target.getSize(cb); int width = 1; int height = 2; LayoutParams layoutParams = new LayoutParams(width, height); view.setLayoutParams(layoutParams); shadowView.setIsLaidOut(true); shadowObserver.setIsAlive(false); shadowObserver.fireOnPreDrawListeners(); verify(cb).onSizeReady(eq(width), eq(height)); } @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullView() { new TestViewTarget(null); } @Test public void testDecreasesDimensionsByViewPadding() { view.setLayoutParams(new LayoutParams(100, 100)); view.setPadding(25, 25, 25, 25); shadowView.setIsLaidOut(true); target.getSize(cb); verify(cb).onSizeReady(50, 50); } @Test public void getSize_withValidWidthAndHeight_notLaidOut_notLayoutRequested_callsSizeReady() { shadowView.setWidth(100).setHeight(100).setIsLaidOut(false); target.getSize(cb); verify(cb).onSizeReady(100, 100); } @Test public void getSize_withLayoutParams_notLaidOut_doesCallSizeReady() { shadowView .setLayoutParams(new LayoutParams(10, 10)) .setWidth(100) .setHeight(100) .setIsLaidOut(false); target.getSize(cb); verify(cb, times(1)).onSizeReady(anyInt(), anyInt()); } @Test public void getSize_withLayoutParams_emptyParams_notLaidOutOrLayoutRequested_callsSizeReady() { shadowView .setLayoutParams(new LayoutParams(0, 0)) .setWidth(100) .setHeight(100) .setIsLaidOut(false); target.getSize(cb); verify(cb).onSizeReady(100, 100); } @Test public void getSize_withValidWidthAndHeight_preV19_layoutRequested_callsSizeReady() { Util.setSdkVersionInt(18); shadowView.setWidth(100).setHeight(100).requestLayout(); target.getSize(cb); verify(cb).onSizeReady(100, 100); } @Test public void getSize_withWidthAndHeightEqualToPadding_doesNotCallSizeReady() { shadowView.setWidth(100).setHeight(100).setIsLaidOut(true); view.setPadding(50, 50, 50, 50); target.getSize(cb); verify(cb, never()).onSizeReady(anyInt(), anyInt()); } private void setDisplayDimens(Integer width, Integer height) { WindowManager windowManager = (WindowManager) ApplicationProvider.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); Display display = Preconditions.checkNotNull(windowManager).getDefaultDisplay(); if (width != null) { Shadows.shadowOf(display).setWidth(width); } if (height != null) { Shadows.shadowOf(display).setHeight(height); } } @Test public void clearOnDetach_onDetach_withNullRequest_doesNothing() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(null); shadowView.callOnAttachedToWindow(); } // This behavior isn't clearly correct, but it doesn't seem like there's any harm to clear an // already cleared request, so we might as well avoid the extra check/complexity in the code. @Test public void clearOnDetach_onDetach_withClearedRequest_clearsRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); shadowView.callOnDetachedFromWindow(); verify(request).clear(); } @Test public void clearOnDetach_onDetach_withRunningRequest_pausesRequestOnce() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); shadowView.callOnDetachedFromWindow(); verify(request).clear(); } @Test public void clearOnDetach_onDetach_afterOnLoadCleared_removesListener() { attachStateTarget.clearOnDetach(); attachStateTarget.onLoadCleared(/* placeholder= */ null); attachStateTarget.setRequest(request); shadowView.callOnDetachedFromWindow(); verify(request, never()).clear(); } @Test public void clearOnDetach_moreThanOnce_registersObserverOnce() { attachStateTarget.clearOnDetach().clearOnDetach(); assertThat(shadowView.attachStateListeners).hasSize(1); } @Test public void clearOnDetach_onDetach_afterMultipleClearOnDetaches_removesListener() { attachStateTarget.clearOnDetach().clearOnDetach().clearOnDetach(); attachStateTarget.onLoadCleared(/* placeholder= */ null); attachStateTarget.setRequest(request); shadowView.callOnDetachedFromWindow(); verify(request, never()).clear(); } // This behavior isn't clearly correct, but it doesn't seem like there's any harm to clear an // already cleared request, so we might as well avoid the extra check/complexity in the code. @Test public void clearOnDetach_onDetach_afterLoadCleared_clearsRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); shadowView.callOnDetachedFromWindow(); verify(request).clear(); } @Test public void clearOnDetach_onAttach_withNullRequest_doesNothing() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(null); shadowView.callOnAttachedToWindow(); } @Test public void clearOnDetach_onAttach_withRunningRequest_doesNotBeginRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(false); shadowView.callOnAttachedToWindow(); verify(request, never()).begin(); } @Test public void clearOnDetach_onAttach_withClearedRequest_beginsRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); shadowView.callOnAttachedToWindow(); verify(request).begin(); } @Test public void clearOnDetach_afterLoadClearedAndRestarted_onAttach_beingsRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); attachStateTarget.onLoadCleared(/* placeholder= */ null); attachStateTarget.onLoadStarted(/* placeholder= */ null); shadowView.callOnAttachedToWindow(); verify(request).begin(); } @Test public void clearOnDetach_onAttach_afterLoadCleared_doesNotBeingRequest() { attachStateTarget.clearOnDetach(); attachStateTarget.setRequest(request); when(request.isCleared()).thenReturn(true); attachStateTarget.onLoadCleared(/* placeholder= */ null); shadowView.callOnAttachedToWindow(); verify(request, never()).begin(); } @Test public void onLoadStarted_withoutClearOnDetach_doesNotAddListener() { attachStateTarget.onLoadStarted(/* placeholder= */ null); assertThat(shadowView.attachStateListeners).isEmpty(); } // containsExactly does not need its result checked. @SuppressWarnings("ResultOfMethodCallIgnored") @Test public void onLoadCleared_withoutClearOnDetach_doesNotRemoveListeners() { OnAttachStateChangeListener expected = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) {} @Override public void onViewDetachedFromWindow(View v) {} }; shadowView.addOnAttachStateChangeListener(expected); attachStateTarget.onLoadCleared(/* placeholder= */ null); assertThat(shadowView.attachStateListeners).containsExactly(expected); } @Implements(ViewTreeObserver.class) public static final class PreDrawShadowViewTreeObserver { private final CopyOnWriteArrayList preDrawListeners = new CopyOnWriteArrayList<>(); private boolean isAlive = true; @SuppressWarnings("unused") @Implementation public void addOnPreDrawListener(OnPreDrawListener listener) { checkIsAlive(); preDrawListeners.add(listener); } @SuppressWarnings("unused") @Implementation public void removeOnPreDrawListener(OnPreDrawListener listener) { checkIsAlive(); preDrawListeners.remove(listener); } @Implementation @SuppressWarnings("WeakerAccess") public boolean isAlive() { return isAlive; } private void checkIsAlive() { if (!isAlive()) { throw new IllegalStateException("ViewTreeObserver is not alive!"); } } void setIsAlive(@SuppressWarnings("SameParameterValue") boolean isAlive) { this.isAlive = isAlive; } void fireOnPreDrawListeners() { for (OnPreDrawListener listener : preDrawListeners) { listener.onPreDraw(); } } List getPreDrawListeners() { return preDrawListeners; } } // Shadows require stronger access and unused values. @SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused"}) @Implements(View.class) public static final class SizedShadowView extends ShadowView { @RealObject private View view; private int width; private int height; private LayoutParams layoutParams; private boolean isLaidOut; private boolean isLayoutRequested; final Set attachStateListeners = new HashSet<>(); public SizedShadowView setWidth(int width) { this.width = width; return this; } public SizedShadowView setHeight(int height) { this.height = height; return this; } @Implementation public void addOnAttachStateChangeListener(OnAttachStateChangeListener listener) { attachStateListeners.add(listener); } @Implementation public void removeOnAttachStateChangeListener(OnAttachStateChangeListener listener) { attachStateListeners.remove(listener); } @Implementation public void onAttachedToWindow() { for (OnAttachStateChangeListener listener : attachStateListeners) { listener.onViewAttachedToWindow(view); } } @Implementation public void onDetachedFromWindow() { for (OnAttachStateChangeListener listener : attachStateListeners) { listener.onViewDetachedFromWindow(view); } } @Override public void callOnAttachedToWindow() { super.callOnAttachedToWindow(); } @Override public void callOnDetachedFromWindow() { super.callOnDetachedFromWindow(); } @Implementation public SizedShadowView setLayoutParams(LayoutParams layoutParams) { this.layoutParams = layoutParams; return this; } @Implementation public SizedShadowView setIsLaidOut(boolean isLaidOut) { this.isLaidOut = isLaidOut; return this; } @Implementation @Override public void requestLayout() { isLayoutRequested = true; } @Implementation public int getWidth() { return width; } @Implementation public int getHeight() { return height; } @Implementation public boolean isLaidOut() { return isLaidOut; } @Implementation public boolean isLayoutRequested() { return isLayoutRequested; } @Implementation public LayoutParams getLayoutParams() { return layoutParams; } } private static final class AttachStateTarget extends ViewTarget { AttachStateTarget(View view) { super(view); } @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) {} } private static final class TestViewTarget extends ViewTarget { TestViewTarget(View view) { super(view); } // We're intentionally avoiding the super call. @SuppressWarnings("MissingSuperCall") @Override public void onResourceReady( @NonNull Object resource, @Nullable Transition transition) { // Avoid calling super. } // We're intentionally avoiding the super call. @SuppressWarnings("MissingSuperCall") @Override public void onLoadCleared(@Nullable Drawable placeholder) { // Avoid calling super. } // We're intentionally avoiding the super call. @SuppressWarnings("MissingSuperCall") @Override public void onLoadStarted(@Nullable Drawable placeholder) { // Avoid calling super. } // We're intentionally avoiding the super call. @SuppressWarnings("MissingSuperCall") @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { // Avoid calling super. } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/transition/DrawableCrossFadeFactoryTest.java ================================================ package com.bumptech.glide.request.transition; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import android.graphics.drawable.Drawable; import com.bumptech.glide.load.DataSource; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class DrawableCrossFadeFactoryTest { private DrawableCrossFadeFactory factory; @SuppressWarnings("unchecked") @Before public void setUp() { factory = new DrawableCrossFadeFactory(100 /*duration*/, false /*isCrossFadeEnabled*/); } @Test public void testReturnsNoAnimationIfFromMemoryCache() { assertEquals( NoTransition.get(), factory.build(DataSource.MEMORY_CACHE, true /*isFirstResource*/)); } @Test public void testReturnsReturnsAnimationIfNotFromMemoryCacheAndIsFirstResource() { assertNotEquals( NoTransition.get(), factory.build(DataSource.DATA_DISK_CACHE, true /*isFirstResource*/)); } @Test public void testReturnsAnimationIfNotFromMemoryCacheAndNotIsFirstResource() { assertNotEquals( NoTransition.get(), factory.build(DataSource.DATA_DISK_CACHE, false /*isFirstResource*/)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/transition/DrawableCrossFadeViewAnimationTest.java ================================================ package com.bumptech.glide.request.transition; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import com.bumptech.glide.request.transition.Transition.ViewAdapter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class DrawableCrossFadeViewAnimationTest { private CrossFadeHarness harness; @Before public void setup() { harness = new CrossFadeHarness(); } @Test public void testIgnoresNullViews() { when(harness.adapter.getView()).thenReturn(null); harness.animation.transition(harness.current, harness.adapter); } @Test public void transition_withNonNullPreviousDrawable_setsTransitionDrawable() { Drawable previous = new ColorDrawable(Color.WHITE); when(harness.adapter.getCurrentDrawable()).thenReturn(previous); harness.animation.transition(harness.current, harness.adapter); verify(harness.adapter).setDrawable(any(TransitionDrawable.class)); } @Test public void transition_withNullPreviousDrawable_setsTransitionDrawable() { harness.animation.transition(harness.current, harness.adapter); verify(harness.adapter).setDrawable(any(TransitionDrawable.class)); } @Test public void transition_withNoCurrentDrawable_returnsTrue() { assertTrue(harness.animation.transition(harness.current, harness.adapter)); } @Test public void transition_withCurrentDrawable_returnsTrue() { Drawable previous = new ColorDrawable(Color.RED); when(harness.adapter.getCurrentDrawable()).thenReturn(previous); assertTrue(harness.animation.transition(harness.current, harness.adapter)); } @SuppressWarnings("unchecked") private static class CrossFadeHarness { final Drawable current = new ColorDrawable(Color.GRAY); final ViewAdapter adapter = mock(ViewAdapter.class); final int duration = 200; final DrawableCrossFadeTransition animation = new DrawableCrossFadeTransition(duration, true /*isCrossFadeEnabled*/); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/transition/ViewAnimationTest.java ================================================ package com.bumptech.glide.request.transition; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.view.animation.Animation; import android.widget.ImageView; import com.bumptech.glide.request.transition.Transition.ViewAdapter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ViewAnimationTest { private ViewTransition viewAnimation; private ViewAdapter adapter; private ImageView view; private ViewTransition.ViewTransitionAnimationFactory viewTransitionAnimationFactory; @SuppressWarnings("unchecked") @Before public void setUp() { viewTransitionAnimationFactory = mock(ViewTransition.ViewTransitionAnimationFactory.class); view = mock(ImageView.class); adapter = mock(ViewAdapter.class); when(adapter.getView()).thenReturn(view); viewAnimation = new ViewTransition<>(viewTransitionAnimationFactory); } @Test public void testClearsAnimationOnAnimate() { viewAnimation.transition(null, adapter); verify(view).clearAnimation(); } @Test public void testAlwaysReturnsFalse() { assertFalse(viewAnimation.transition(null, adapter)); } @Test public void testStartsAnimationOnAnimate() { Animation animation = mock(Animation.class); when(viewTransitionAnimationFactory.build(anyContextOrNull())).thenReturn(animation); viewAnimation.transition(null, adapter); verify(view).clearAnimation(); verify(view).startAnimation(eq(animation)); } private static Context anyContextOrNull() { return any(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/transition/ViewPropertyAnimationTest.java ================================================ package com.bumptech.glide.request.transition; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.view.View; import android.widget.ImageView; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.request.transition.Transition.ViewAdapter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ViewPropertyAnimationTest { private ViewPropertyTransition.Animator animator; private ViewPropertyTransition animation; @Before public void setUp() { animator = mock(ViewPropertyTransition.Animator.class); animation = new ViewPropertyTransition<>(animator); } @Test public void testAlwaysReturnsFalse() { assertFalse(animation.transition(new Object(), mock(ViewAdapter.class))); } @Test public void testCallsAnimatorWithGivenView() { ImageView view = new ImageView(ApplicationProvider.getApplicationContext()); ViewAdapter adapter = mock(ViewAdapter.class); when(adapter.getView()).thenReturn(view); animation.transition(new Object(), adapter); verify(animator).animate(eq(view)); } @Test public void testDoesNotCallAnimatorIfGivenAdapterWithNullView() { ViewAdapter adapter = mock(ViewAdapter.class); animation.transition(new Object(), adapter); verify(animator, never()).animate(any(View.class)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/transition/ViewPropertyViewTransitionAnimationFactoryTest.java ================================================ package com.bumptech.glide.request.transition; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.mockito.Mockito.mock; import com.bumptech.glide.load.DataSource; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ViewPropertyViewTransitionAnimationFactoryTest { private ViewPropertyAnimationFactory factory; @Before public void setUp() { ViewPropertyTransition.Animator animator = mock(ViewPropertyTransition.Animator.class); factory = new ViewPropertyAnimationFactory<>(animator); } @Test public void testReturnsNoAnimationIfFromMemoryCache() { assertEquals( NoTransition.get(), factory.build(DataSource.MEMORY_CACHE, true /*isFirstResource*/)); } @Test public void testReturnsNoAnimationIfNotFirstResource() { assertEquals( NoTransition.get(), factory.build(DataSource.DATA_DISK_CACHE, false /*isFirstResource*/)); } @Test public void testReturnsAnimationIfNotFromMemoryCacheAndFirstResource() { assertNotEquals( NoTransition.get(), factory.build(DataSource.DATA_DISK_CACHE, true /*isFirstResource*/)); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/request/transition/ViewTransitionAnimationFactoryTest.java ================================================ package com.bumptech.glide.request.transition; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.view.View; import android.view.animation.Animation; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.DataSource; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class ViewTransitionAnimationFactoryTest { private ViewTransition.ViewTransitionAnimationFactory viewTransitionAnimationFactory; private ViewAnimationFactory factory; @Before public void setUp() { viewTransitionAnimationFactory = mock(ViewTransition.ViewTransitionAnimationFactory.class); factory = new ViewAnimationFactory<>(viewTransitionAnimationFactory); } @Test public void testFactoryReturnsNoAnimationIfFromMemoryCache() { Transition animation = factory.build(DataSource.MEMORY_CACHE, true /*isFirstResource*/); assertEquals(NoTransition.get(), animation); verify(viewTransitionAnimationFactory, never()) .build(ApplicationProvider.getApplicationContext()); } @Test public void testFactoryReturnsNoAnimationIfNotFirstResource() { Transition animation = factory.build(DataSource.DATA_DISK_CACHE, false /*isFirstResource*/); assertEquals(NoTransition.get(), animation); verify(viewTransitionAnimationFactory, never()) .build(ApplicationProvider.getApplicationContext()); } @Test public void testFactoryReturnsActualAnimationIfNotIsFromMemoryCacheAndIsFirstResource() { Transition transition = factory.build(DataSource.DATA_DISK_CACHE, true /*isFirstResource*/); Animation animation = mock(Animation.class); when(viewTransitionAnimationFactory.build(anyContextOrNull())).thenReturn(animation); Transition.ViewAdapter adapter = mock(Transition.ViewAdapter.class); View view = mock(View.class); when(adapter.getView()).thenReturn(view); transition.transition(new Object(), adapter); verify(view).startAnimation(eq(animation)); } private static Context anyContextOrNull() { return any(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/resize/load/ExifTest.java ================================================ package com.bumptech.glide.resize.load; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool; import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser; import com.bumptech.glide.testutil.TestResourceUtil; import java.io.IOException; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ExifTest { private ArrayPool byteArrayPool; private InputStream open(String imageName) { return TestResourceUtil.openResource(getClass(), imageName); } private void assertOrientation(String filePrefix, int expectedOrientation) { InputStream is = null; try { is = open(filePrefix + "_" + expectedOrientation + ".jpg"); assertEquals( new DefaultImageHeaderParser().getOrientation(is, byteArrayPool), expectedOrientation); } catch (IOException e) { throw new RuntimeException(e); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // Do nothing. } } } } @Before public void setUp() { byteArrayPool = new LruArrayPool(); } @Test public void testIssue387() throws IOException { InputStream is = TestResourceUtil.openResource(getClass(), "issue387_rotated_jpeg.jpg"); assertThat(new DefaultImageHeaderParser().getOrientation(is, byteArrayPool)).isEqualTo(6); } @Test public void testLandscape() throws IOException { for (int i = 1; i <= 8; i++) { assertOrientation("Landscape", i); } } @Test public void testPortrait() throws IOException { for (int i = 1; i <= 8; i++) { assertOrientation("Portrait", i); } } @Test public void testHandlesInexactSizesInByteArrayPools() { for (int i = 1; i <= 8; i++) { byteArrayPool.put(new byte[ArrayPool.STANDARD_BUFFER_SIZE_BYTES]); assertOrientation("Portrait", i); } for (int i = 1; i <= 8; i++) { byteArrayPool.put(new byte[ArrayPool.STANDARD_BUFFER_SIZE_BYTES]); assertOrientation("Landscape", i); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/signature/AndroidResourceSignatureTest.java ================================================ package com.bumptech.glide.signature; import static org.junit.Assert.assertNotNull; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Key; import com.bumptech.glide.tests.KeyTester; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.Shadows; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = 28) public class AndroidResourceSignatureTest { @Rule public final KeyTester keyTester = new KeyTester(); private Context context; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); } @Test public void testCanGetKeyForSignature() { Key key = AndroidResourceSignature.obtain(context); assertNotNull(key); } @Test public void testKeyForSignatureIsTheSameAcrossCallsInTheSamePackage() { keyTester .addEquivalenceGroup( AndroidResourceSignature.obtain(context), AndroidResourceSignature.obtain(context)) .addEquivalenceGroup(new ObjectKey("test")) .addRegressionTest( ApplicationVersionSignature.obtain(context), "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9") .test(); } @Test public void testKeyForSignatureDiffersByNightMode() { RuntimeEnvironment.setQualifiers("notnight"); keyTester .addEquivalenceGroup( AndroidResourceSignature.obtain(context), AndroidResourceSignature.obtain(context)) .addRegressionTest( AndroidResourceSignature.obtain(context), "265d958bdae1bea56e45cc31f4db672c22893b66fef85617bbc78742bd912207"); RuntimeEnvironment.setQualifiers("night"); keyTester .addEquivalenceGroup( AndroidResourceSignature.obtain(context), AndroidResourceSignature.obtain(context)) .addRegressionTest( AndroidResourceSignature.obtain(context), "96c9b8d5bb071ccd67df50cd9a0059640ebd02db78d08f07611ec145ce44a638"); keyTester.test(); } @Test public void testMissingPackageInfo() throws NameNotFoundException { // Make getPackageInfo throw NameNotFoundException. Shadows.shadowOf(context.getPackageManager()).removePackage(context.getPackageName()); Key key = AndroidResourceSignature.obtain(context); assertNotNull(key); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/signature/ApplicationVersionSignatureTest.java ================================================ package com.bumptech.glide.signature; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import androidx.test.core.app.ApplicationProvider; import com.bumptech.glide.load.Key; import com.bumptech.glide.tests.KeyTester; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ApplicationVersionSignatureTest { @Rule public final KeyTester keyTester = new KeyTester(); private Context context; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); } @After public void tearDown() { ApplicationVersionSignature.reset(); } @Test public void testCanGetKeyForSignature() { Key key = ApplicationVersionSignature.obtain(context); assertNotNull(key); } @Test public void testKeyForSignatureIsTheSameAcrossCallsInTheSamePackage() throws NoSuchAlgorithmException, UnsupportedEncodingException { keyTester .addEquivalenceGroup( ApplicationVersionSignature.obtain(context), ApplicationVersionSignature.obtain(context)) .addEquivalenceGroup(new ObjectKey("test")) .addRegressionTest( ApplicationVersionSignature.obtain(context), "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9") .test(); } @Test public void testUnresolvablePackageInfo() throws NameNotFoundException { Context context = mock(Context.class, Answers.RETURNS_DEEP_STUBS); String packageName = "my.package"; when(context.getPackageName()).thenReturn(packageName); when(context.getPackageManager().getPackageInfo(packageName, 0)) .thenThrow(new NameNotFoundException("test")); Key key = ApplicationVersionSignature.obtain(context); assertNotNull(key); } @Test public void testMissingPackageInfo() throws NameNotFoundException { Context context = mock(Context.class, Answers.RETURNS_DEEP_STUBS); String packageName = "my.package"; when(context.getPackageName()).thenReturn(packageName); when(context.getPackageManager().getPackageInfo(packageName, 0)).thenReturn(null); Key key = ApplicationVersionSignature.obtain(context); assertNotNull(key); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/signature/EmptySignatureTest.java ================================================ package com.bumptech.glide.signature; import static org.mockito.Mockito.mock; import com.bumptech.glide.load.Key; import com.bumptech.glide.tests.KeyTester; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class EmptySignatureTest { @Rule public final KeyTester keyTester = new KeyTester(); @Test public void testEquals() { keyTester .addEquivalenceGroup(EmptySignature.obtain(), EmptySignature.obtain()) .addEquivalenceGroup(mock(Key.class)) .addEmptyDigestRegressionTest(EmptySignature.obtain()) .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/signature/MediaStoreSignatureTest.java ================================================ package com.bumptech.glide.signature; import com.bumptech.glide.tests.KeyTester; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class MediaStoreSignatureTest { @Rule public final KeyTester keyTester = new KeyTester(); @Test public void equalsHashCodeAndDigest() { keyTester .addEquivalenceGroup( new MediaStoreSignature("first", 100, 1), new MediaStoreSignature("first", 100, 1)) .addEquivalenceGroup(new MediaStoreSignature("second", 100, 1)) .addEquivalenceGroup(new MediaStoreSignature("first", 200, 1)) .addEquivalenceGroup(new MediaStoreSignature("first", 100, 2)) .addRegressionTest( new MediaStoreSignature("first", 100, 1), "04959925006b21081000fd10835cc376343c0e922df0bd7346897ede6f958adf") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/signature/ObjectKeyTest.java ================================================ package com.bumptech.glide.signature; import com.bumptech.glide.tests.KeyTester; import java.security.NoSuchAlgorithmException; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ObjectKeyTest { @Rule public final KeyTester keyTester = new KeyTester(); @Test public void testEqualsHashCodeAndDigest() throws NoSuchAlgorithmException { Object object = new Object(); keyTester .addEquivalenceGroup(new ObjectKey(object), new ObjectKey(object)) .addEquivalenceGroup(new ObjectKey(new Object())) .addEquivalenceGroup(new ObjectKey("test"), new ObjectKey("test")) .addRegressionTest( new ObjectKey("test"), "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08") .test(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/tests/BackgroundUtil.java ================================================ package com.bumptech.glide.tests; public final class BackgroundUtil { private BackgroundUtil() { // Utility class. } public static void testInBackground(BackgroundTester test) throws InterruptedException { TestThread thread = new TestThread(test); thread.start(); thread.join(); if (thread.exception != null) { throw new RuntimeException(thread.exception); } } private static final class TestThread extends Thread { private final BackgroundTester test; private Exception exception; private TestThread(BackgroundTester test) { this.test = test; } @Override public void run() { super.run(); try { test.runTest(); } catch (Exception e) { exception = e; } } } public interface BackgroundTester { void runTest(); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/tests/ContentResolverShadow.java ================================================ package com.bumptech.glide.tests; import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.net.Uri; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @Implements(ContentResolver.class) public class ContentResolverShadow { private final Map fileDescriptorMap = new HashMap<>(); private final Map inputStreamMap = new HashMap<>(); public void registerFileDescriptor(Uri uri, AssetFileDescriptor fileDescriptor) { fileDescriptorMap.put(uri, fileDescriptor); } public void registerInputStream(Uri uri, InputStream inputStream) { inputStreamMap.put(uri, inputStream); } @Implementation public InputStream openInputStream(Uri uri) { return inputStreamMap.get(uri); } @Implementation public AssetFileDescriptor openAssetFileDescriptor(Uri uri, String mode) { return fileDescriptorMap.get(uri); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/tests/GlideShadowLog.java ================================================ package com.bumptech.glide.tests; import android.util.Log; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowLog; /** * Exists only to "enable" logging for test coverage. * *

    TODO: when we can ignore Log.* via configuration, remove this class. */ @Implements(Log.class) public class GlideShadowLog extends ShadowLog { @Implementation public static boolean isLoggable(String tag, int level) { return true; } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/tests/KeyTester.java ================================================ package com.bumptech.glide.tests; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; import static org.junit.Assert.fail; import androidx.annotation.CheckResult; import androidx.annotation.NonNull; import com.bumptech.glide.load.Key; import com.google.common.base.Equivalence; import com.google.common.testing.EquivalenceTester; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public final class KeyTester implements TestRule { private static final String EMPTY_DIGEST_STRING = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; private final List regressionTests = new ArrayList<>(); private final Sha256 sha256 = new Sha256(); private final EquivalenceTester tester = EquivalenceTester.of(new KeyEquivalence(sha256)); private boolean isUsedWithoutCallingTest; private boolean isUsedAsRule; @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { isUsedAsRule = true; base.evaluate(); if (isUsedWithoutCallingTest) { fail("You used KeyTester but failed to call test()!"); } } }; } private void assertUsedAsRule() { if (!isUsedAsRule) { fail("You must use KeyTester as an @Rule"); } } @CheckResult public KeyTester addEquivalenceGroup(Key first, Key... rest) { assertUsedAsRule(); isUsedWithoutCallingTest = true; tester.addEquivalenceGroup(first, rest); return this; } @CheckResult public KeyTester addRegressionTest(Key key, String expectedDigest) { assertUsedAsRule(); if (EMPTY_DIGEST_STRING.equals(expectedDigest)) { throw new IllegalArgumentException( "Expected digest is empty, if this is intended use " + "addEmptyDigestRegressionTest instead"); } return addRegressionTestInternal(key, expectedDigest); } @CheckResult public KeyTester addEmptyDigestRegressionTest(Key key) { assertUsedAsRule(); return addRegressionTestInternal(key, EMPTY_DIGEST_STRING); } private KeyTester addRegressionTestInternal(Key key, String expectedDigest) { isUsedWithoutCallingTest = true; regressionTests.add(new KeyAndHash(key, expectedDigest)); return this; } public void test() { assertUsedAsRule(); isUsedWithoutCallingTest = false; tester.test(); assertThat(regressionTests).isNotEmpty(); int i = 1; for (KeyAndHash keyAndHash : regressionTests) { assert_() .withMessage( "Unexpected digest for regression test [" + i + "]: with key: " + keyAndHash.key) .that(sha256.getStringDigest(keyAndHash.key)) .isEqualTo(keyAndHash.hash); i++; } } private static final class Sha256 { private final MessageDigest digest; Sha256() { try { digest = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } private byte[] getDigest(Key key) { try { key.updateDiskCacheKey(digest); return digest.digest(); } finally { digest.reset(); } } String getStringDigest(Key key) { return com.bumptech.glide.util.Util.sha256BytesToHex(getDigest(key)); } } /** Tests equals, hashcode and digest methods of {@link Key}s. */ private static final class KeyEquivalence extends Equivalence { private final Sha256 sha256; KeyEquivalence(Sha256 sha256) { this.sha256 = sha256; } @Override protected boolean doEquivalent(@NonNull Key a, @NonNull Key b) { byte[] aDigest = sha256.getDigest(a); byte[] bDigest = sha256.getDigest(b); Object object = new Object(); Object sentinel = null; return a.equals(b) && b.equals(a) && !a.equals(sentinel) && !b.equals(sentinel) && !a.equals(object) && !b.equals(object) && Arrays.equals(aDigest, bDigest); } @Override protected int doHash(@NonNull Key key) { return key.hashCode(); } } private static final class KeyAndHash { private final Key key; private final String hash; KeyAndHash(Key key, String hash) { this.key = key; this.hash = hash; } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/tests/TearDownGlide.java ================================================ package com.bumptech.glide.tests; import com.bumptech.glide.Glide; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; /** Clears out Glide's disk cache and the Glide singleton after every test method. */ public final class TearDownGlide implements TestRule { @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { base.evaluate(); } finally { Glide.tearDown(); } } }; } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/tests/Util.java ================================================ package com.bumptech.glide.tests; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.RETURNS_DEFAULTS; import static org.mockito.Mockito.mock; import android.content.Context; import android.graphics.Bitmap; import android.os.Build; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.engine.Resource; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.util.ReflectionHelpers; // FIXME move to testutil module public class Util { /** * Gives the proper generic type to the {@link ArgumentCaptor}. Only useful when the captor's * {@code T} is also a generic type. Without this it's really ugly to have a properly typed captor * object. */ @SuppressWarnings("unchecked") public static ArgumentCaptor cast(ArgumentCaptor captor) { return (ArgumentCaptor) captor; } public static DataSource isADataSource() { return isA(DataSource.class); } public static Context anyContext() { return any(); } /** * Creates a Mockito argument matcher to be used in verify. It returns a generic typed {@link * Resource} to prevent unchecked warnings. */ @SuppressWarnings("unchecked") public static Resource anyResource() { return any(Resource.class); } /** * Creates a Mockito mock object. It returns a generic typed {@link Resource} to prevent unchecked * warnings. */ @SuppressWarnings("unchecked") public static Resource mockResource() { return mock(Resource.class); } public static boolean isWindows() { return System.getProperty("os.name").startsWith("Windows"); } public static void writeFile(File file, byte[] data) throws IOException { OutputStream out = new FileOutputStream(file); try { out.write(data); out.flush(); out.close(); } finally { try { out.close(); } catch (IOException ex) { // Do nothing. } } } public static byte[] readFile(File file, int expectedLength) throws IOException { InputStream is = new FileInputStream(file); byte[] result = new byte[expectedLength]; try { assertEquals(expectedLength, is.read(result)); assertEquals(-1, is.read()); } finally { try { is.close(); } catch (IOException e) { // Do nothing. } } return result; } public static void setSdkVersionInt(int version) { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", version); } public static final class WriteDigest implements Answer { private final String toWrite; public WriteDigest(String toWrite) { this.toWrite = toWrite; } @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { MessageDigest md = (MessageDigest) invocationOnMock.getArguments()[0]; md.update(toWrite.getBytes("UTF-8")); return null; } } public static final class ReturnsSelfAnswer implements Answer { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object mock = invocation.getMock(); if (invocation.getMethod().getReturnType().isInstance(mock)) { return mock; } else { return RETURNS_DEFAULTS.answer(invocation); } } } public static final class CallDataReady implements Answer { private final T data; public CallDataReady(T data) { this.data = data; } @SuppressWarnings("unchecked") @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { DataFetcher.DataCallback callback = (DataFetcher.DataCallback) invocationOnMock.getArguments()[1]; callback.onDataReady(data); return null; } } public static final class CreateBitmap implements Answer { @Override public Bitmap answer(InvocationOnMock invocation) throws Throwable { int width = (Integer) invocation.getArguments()[0]; int height = (Integer) invocation.getArguments()[1]; Bitmap.Config config = (Bitmap.Config) invocation.getArguments()[2]; return Bitmap.createBitmap(width, height, config); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/util/ByteBufferUtilTest.java ================================================ package com.bumptech.glide.util; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static org.junit.Assert.assertEquals; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ByteBufferUtilTest { private static final int BUFFER_SIZE = 16384; @Test public void testFromStream_small() throws IOException { testFromStream(4); } @Test public void testFromStream_empty() throws IOException { testFromStream(0); } @Test public void testFromStream_bufferAndAHalf() throws IOException { testFromStream(BUFFER_SIZE + BUFFER_SIZE / 2); } @Test public void testFromStream_massive() throws IOException { testFromStream(12 * BUFFER_SIZE + 12345); } /** All tests are basically the same thing but with different amounts of data. */ private void testFromStream(int dataLength) throws IOException { byte[] bytes = createByteData(dataLength); InputStream byteStream = new ByteArrayInputStream(bytes); ByteBuffer byteBuffer = ByteBufferUtil.fromStream(byteStream); assertByteBufferContents(byteBuffer, bytes); byteStream.close(); } private byte[] createByteData(int size) { byte[] bytes = new byte[size]; // Put some arbitrary bytes in there. for (int i = 0; i < size; i++) { bytes[i] = (byte) (i % 4); } return bytes; } private void assertByteBufferContents(ByteBuffer buffer, byte[] expectedBytes) { assertEquals(expectedBytes.length, buffer.limit()); for (int i = 0; i < expectedBytes.length; i++) { assertEquals(expectedBytes[i], buffer.get(i)); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/util/ContentLengthInputStreamTest.java ================================================ package com.bumptech.glide.util; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class ContentLengthInputStreamTest { @Mock private InputStream wrapped; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void testAvailable_withZeroReadsAndValidContentLength_returnsContentLength() throws IOException { int value = 123356; InputStream is = ContentLengthInputStream.obtain(wrapped, String.valueOf(value)); assertThat(is.available()).isEqualTo(value); } @Test public void testAvailable_withNullContentLength_returnsWrappedAvailable() throws IOException { InputStream is = ContentLengthInputStream.obtain(wrapped, null /*contentLengthHeader*/); int expected = 1234; when(wrapped.available()).thenReturn(expected); assertThat(is.available()).isEqualTo(expected); } @Test public void testAvailable_withInvalidContentLength_returnsWrappedAvailable() throws IOException { InputStream is = ContentLengthInputStream.obtain(wrapped, "invalid_length"); int expected = 567; when(wrapped.available()).thenReturn(expected); assertThat(is.available()).isEqualTo(expected); } @Test public void testAvailable_withRead_returnsContentLengthOffsetByRead() throws IOException { int contentLength = 999; InputStream is = ContentLengthInputStream.obtain(wrapped, String.valueOf(contentLength)); when(wrapped.read()).thenReturn(1); assertThat(is.read()).isEqualTo(1); assertThat(is.available()).isEqualTo(contentLength - 1); } @Test public void testAvailable_handlesReadValueOfZero() throws IOException { int contentLength = 999; InputStream is = ContentLengthInputStream.obtain(wrapped, String.valueOf(contentLength)); when(wrapped.read()).thenReturn(0); assertThat(is.read()).isEqualTo(0); assertThat(is.available()).isEqualTo(contentLength - 1); } @Test public void testAvailable_withReadBytes_returnsContentLengthOffsetByNumberOfBytes() throws IOException { int contentLength = 678; InputStream is = ContentLengthInputStream.obtain(wrapped, String.valueOf(contentLength)); int read = 100; when(wrapped.read(any(byte[].class), anyInt(), anyInt())).thenReturn(read); assertThat(is.read(new byte[500], 0, 0)).isEqualTo(read); assertThat(is.available()).isEqualTo(contentLength - read); } @Test public void testRead_whenReturnsLessThanZeroWithoutReadingAllContent_throwsIOException() throws IOException { int contentLength = 1; InputStream is = ContentLengthInputStream.obtain(wrapped, String.valueOf(contentLength)); when(wrapped.read()).thenReturn(-1); try { //noinspection ResultOfMethodCallIgnored is.read(); fail("Failed to throw expected exception"); } catch (IOException e) { // Expected. } } @Test public void testReadBytes_whenReturnsLessThanZeroWithoutReadingAllContent_throwsIOException() throws IOException { int contentLength = 2; InputStream is = ContentLengthInputStream.obtain(wrapped, String.valueOf(contentLength)); when(wrapped.read(any(byte[].class), anyInt(), anyInt())).thenReturn(-1); try { //noinspection ResultOfMethodCallIgnored is.read(new byte[10], 0, 0); fail("Failed to throw expected exception"); } catch (IOException e) { // Expected. } } @Test public void testRead_whenReturnsLessThanZeroWithInvalidLength_doesNotThrow() throws IOException { InputStream is = ContentLengthInputStream.obtain(wrapped, "invalid_length"); when(wrapped.read()).thenReturn(-1); //noinspection ResultOfMethodCallIgnored is.read(); } @Test public void testReadBytes_whenReturnsLessThanZeroWithInvalidLength_doesNotThrow() throws IOException { InputStream is = ContentLengthInputStream.obtain(wrapped, "invalid_length"); when(wrapped.read(any(byte[].class), anyInt(), anyInt())).thenReturn(-1); //noinspection ResultOfMethodCallIgnored is.read(new byte[10], 0, 0); } @Test public void testRead_readWithZeroes_doesNotThrow() throws IOException { ByteArrayInputStream inner = new ByteArrayInputStream(new byte[] {0, 0, 0}); InputStream is = ContentLengthInputStream.obtain(inner, 3); assertThat(is.read()).isEqualTo(0); assertThat(is.read()).isEqualTo(0); assertThat(is.read()).isEqualTo(0); assertThat(is.read()).isEqualTo(-1); } @Test public void testRead_readWithHighValues_doesNotThrow() throws IOException { ByteArrayInputStream inner = new ByteArrayInputStream(new byte[] {(byte) 0xF0, (byte) 0xA0, (byte) 0xFF}); InputStream is = ContentLengthInputStream.obtain(inner, 3); assertThat(is.read()).isEqualTo(0xF0); assertThat(is.read()).isEqualTo(0xA0); assertThat(is.read()).isEqualTo(0xFF); assertThat(is.read()).isEqualTo(-1); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/util/ExceptionPassthroughInputStreamTest.java ================================================ package com.bumptech.glide.util; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.SocketTimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ExceptionPassthroughInputStreamTest { private final InputStream validInputStream = new ByteArrayInputStream( new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, }); private final InputStream throwingInputStream = new ExceptionThrowingInputStream(); private ExceptionPassthroughInputStream validWrapper; private ExceptionPassthroughInputStream throwingWrapper; @Before public void setUp() throws Exception { validWrapper = new ExceptionPassthroughInputStream(); validWrapper.setInputStream(validInputStream); throwingWrapper = new ExceptionPassthroughInputStream(); throwingWrapper.setInputStream(throwingInputStream); } @After public void tearDown() { ExceptionPassthroughInputStream.clearQueue(); } @Test public void testReturnsWrappedAvailable() throws IOException { assertEquals(validInputStream.available(), validWrapper.available()); } @Test public void testCallsCloseOnWrapped() throws IOException { ExceptionPassthroughInputStream wrapper = new ExceptionPassthroughInputStream(); final AtomicBoolean isClosed = new AtomicBoolean(); wrapper.setInputStream( new InputStream() { @Override public int read() { return 0; } @Override public void close() throws IOException { super.close(); isClosed.set(true); } }); wrapper.close(); assertThat(isClosed.get()).isTrue(); } @Test public void testCallsMarkOnWrapped() throws IOException { int toMark = 5; validWrapper.mark(toMark); assertThat(validWrapper.read(new byte[5], 0, 5)).isEqualTo(5); validInputStream.reset(); assertThat(validInputStream.read()).isEqualTo(0); } @Test public void testReturnsWrappedMarkSupported() { assertTrue(validWrapper.markSupported()); } @Test public void testCallsReadByteArrayOnWrapped() throws IOException { byte[] buffer = new byte[8]; assertEquals(buffer.length, validWrapper.read(buffer)); } @Test public void testCallsReadArrayWithOffsetAndCountOnWrapped() throws IOException { int offset = 1; int count = 4; byte[] buffer = new byte[5]; assertEquals(count, validWrapper.read(buffer, offset, count)); } @Test public void testCallsReadOnWrapped() throws IOException { assertEquals(0, validWrapper.read()); assertEquals(1, validWrapper.read()); assertEquals(2, validWrapper.read()); } @Test public void testCallsResetOnWrapped() throws IOException { validWrapper.mark(5); assertThat(validWrapper.read()).isEqualTo(0); assertThat(validWrapper.read()).isEqualTo(1); validWrapper.reset(); assertThat(validWrapper.read()).isEqualTo(0); } @Test public void testCallsSkipOnWrapped() throws IOException { int toSkip = 5; assertThat(validWrapper.skip(toSkip)).isEqualTo(toSkip); assertThat(validWrapper.read()).isEqualTo(5); } @Test public void testCatchesExceptionOnRead() { SocketTimeoutException expected = assertThrows( SocketTimeoutException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { throwingWrapper.read(); } }); assertEquals(expected, throwingWrapper.getException()); } @Test public void testCatchesExceptionOnReadBuffer() { SocketTimeoutException exception = assertThrows( SocketTimeoutException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { throwingWrapper.read(new byte[1]); } }); assertEquals(exception, throwingWrapper.getException()); } @Test public void testCatchesExceptionOnReadBufferWithOffsetAndCount() { SocketTimeoutException exception = assertThrows( SocketTimeoutException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { throwingWrapper.read(new byte[2], 1, 1); } }); assertEquals(exception, throwingWrapper.getException()); } @Test public void testCatchesExceptionOnSkip() { SocketTimeoutException exception = assertThrows( SocketTimeoutException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { throwingWrapper.skip(100); } }); assertEquals(exception, throwingWrapper.getException()); } @Test public void testExceptionIsNotSetInitially() { assertNull(validWrapper.getException()); } @SuppressWarnings("ResultOfMethodCallIgnored") @Test public void testResetsExceptionToNullOnRelease() { assertThrows( SocketTimeoutException.class, new ThrowingRunnable() { @Override public void run() throws Throwable { throwingWrapper.read(); } }); throwingWrapper.release(); assertNull(validWrapper.getException()); } @Test public void testCanReleaseAnObtainFromPool() { validWrapper.release(); InputStream fromPool = ExceptionPassthroughInputStream.obtain(validInputStream); assertEquals(validWrapper, fromPool); } @Test public void testCanObtainNewStreamFromPool() throws IOException { InputStream fromPool = ExceptionPassthroughInputStream.obtain(validInputStream); int read = fromPool.read(); assertEquals(0, read); } private static final class ExceptionThrowingInputStream extends InputStream { @Override public int read() throws IOException { throw new SocketTimeoutException(); } } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/util/FixedPreloadSizeProviderTest.java ================================================ package com.bumptech.glide.util; import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK; import static com.google.common.truth.Truth.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = ROBOLECTRIC_SDK) public class FixedPreloadSizeProviderTest { // containsExactly doesn't need a return value check. @SuppressWarnings("ResultOfMethodCallIgnored") @Test public void testReturnsGivenSize() { int width = 500; int height = 1234; FixedPreloadSizeProvider provider = new FixedPreloadSizeProvider<>(width, height); int[] size = provider.getPreloadSize(new Object(), 0, 0); assertThat(size).asList().containsExactly(width, height); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/util/MarkEnforcingInputStreamTest.java ================================================ package com.bumptech.glide.util; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import java.io.ByteArrayInputStream; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class MarkEnforcingInputStreamTest { // An arbitrary number > 0. private static final int MARK_LIMIT = 5; // Another arbitrary number > MARK_LIMIT. private static final int DATA_SIZE = MARK_LIMIT + 1; @Test public void testReturnsByte_whenReadsUpToMarkLimit_withMoreBytesAvailable() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); for (int i = 0; i < MARK_LIMIT; i++) { assertThat(is.read()).isAtLeast(0); } } @Test public void testReturnsByte_whenReadsUpToMarkLimit_withNoMoreBytesAvailable() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[MARK_LIMIT])); for (int i = 0; i < MARK_LIMIT; i++) { assertThat(is.read()).isAtLeast(0); } } @Test public void testReturnsEndOfStream_whenReadsSingleBytePastMarkLimit() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); for (int i = 0; i < MARK_LIMIT; i++) { assertThat(is.read()).isAtLeast(0); } assertEquals(-1, is.read()); } @Test public void testOverridesByteCount_whenReadBufferLargerThanMarkLimit_withNonZeroBytesRemainingInMarkLimit() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); byte[] buffer = new byte[DATA_SIZE]; assertEquals(MARK_LIMIT, is.read(buffer)); } @Test public void testReturnsEndOfStream_whenReadBufferLargerThanMarkLimit_withZeroBytesRemainingInMarkLimit() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); byte[] buffer = new byte[MARK_LIMIT]; assertEquals(MARK_LIMIT, is.read(buffer)); assertEquals(-1, is.read(buffer)); } @Test public void testDoesNotReadIntoBuffer_withZeroBytesRemainingInMarkLimit() throws IOException { byte[] expected = new byte[MARK_LIMIT]; for (int i = 0; i < MARK_LIMIT; i++) { expected[i] = (byte) (i + 1); } byte[] buffer = new byte[MARK_LIMIT]; System.arraycopy(expected, 0, buffer, 0, MARK_LIMIT); // All zeros. MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); for (int i = 0; i < MARK_LIMIT; i++) { assertThat(is.read()).isAtLeast(0); } assertEquals(-1, is.read(buffer)); assertThat(buffer).isEqualTo(expected); } @Test public void testResetUnsetsLimit() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); for (int i = 0; i < MARK_LIMIT; i++) { assertThat(is.read()).isAtLeast(0); } is.reset(); for (int i = 0; i < DATA_SIZE; i++) { assertThat(is.read()).isAtLeast(0); } } @Test public void testOverridesByteCount_whenSkipCountLargerThanMarkLimit_withNonZeroBytesRemainingInMarkLimit() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); assertEquals(MARK_LIMIT, is.skip(DATA_SIZE)); } @Test public void testReturnsEndOfStream_whenSkipping_withZeroBytesRemainingInMarkLimit() throws IOException { MarkEnforcingInputStream is = new MarkEnforcingInputStream(new ByteArrayInputStream(new byte[DATA_SIZE])); is.mark(MARK_LIMIT); assertEquals(MARK_LIMIT, is.skip(DATA_SIZE)); assertEquals(0, is.skip(1)); } @Test public void testReturnsStreamAvailable_whenMarkIsNotSet() throws IOException { ByteArrayInputStream wrapped = new ByteArrayInputStream(new byte[MARK_LIMIT]); MarkEnforcingInputStream is = new MarkEnforcingInputStream(wrapped); assertEquals(wrapped.available(), is.available()); } @Test public void testReturnsStreamAvailable_whenMarkIsSet_withMarkGreaterThanStreamAvailable() throws IOException { ByteArrayInputStream wrapped = new ByteArrayInputStream(new byte[MARK_LIMIT]); MarkEnforcingInputStream is = new MarkEnforcingInputStream(wrapped); is.mark(wrapped.available() + 1); assertEquals(wrapped.available(), is.available()); } @Test public void testReturnsMarkLimitAsAvailable_whenMarkIsSet_withMarkLessThanStreamAvailable() throws IOException { ByteArrayInputStream wrapped = new ByteArrayInputStream(new byte[MARK_LIMIT]); MarkEnforcingInputStream is = new MarkEnforcingInputStream(wrapped); int expected = wrapped.available() - 1; is.mark(expected); assertEquals(expected, is.available()); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/util/UtilTest.java ================================================ package com.bumptech.glide.util; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import android.graphics.Bitmap; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = 27) public class UtilTest { @Test public void testReturnsCorrectBitmapSizeForDifferentDimensions() { int width = 100; int height = 100; Bitmap.Config config = Bitmap.Config.ARGB_8888; int initialSize = Util.getBitmapByteSize(width, height, config); int sizeOne = Util.getBitmapByteSize(width * 2, height, config); int sizeTwo = Util.getBitmapByteSize(width, height * 2, config); assertEquals(4 * width * height, initialSize); assertEquals(2 * initialSize, sizeOne); assertEquals(2 * initialSize, sizeTwo); } @Test public void testReturnsCorrectBitmapSizeForAlpha8Bitmap() { int width = 110; int height = 43; int size = Util.getBitmapByteSize(width, height, Bitmap.Config.ALPHA_8); assertEquals(width * height, size); } @Test public void testReturnsCorrectBitmapSizeForRgb565() { int width = 34; int height = 1444; int size = Util.getBitmapByteSize(width, height, Bitmap.Config.RGB_565); assertEquals(width * height * 2, size); } @Test public void testReturnsCorrectBitmapSizeForARGB4444() { int width = 4454; int height = 1235; int size = Util.getBitmapByteSize(width, height, Bitmap.Config.ARGB_4444); assertEquals(width * height * 2, size); } @Test public void testReturnsCorrectBitmapSizeForARGB8888() { int width = 943; int height = 3584; int size = Util.getBitmapByteSize(width, height, Bitmap.Config.ARGB_8888); assertEquals(width * height * 4, size); } @Test public void testReturnsLargestSizeForNullConfig() { int width = 999; int height = 41324; int size = Util.getBitmapByteSize(width, height, null); assertEquals(width * height * 4, size); } @Test public void getBitmapByteSize_withRGBA_F16_returnsCorrectSize() { int width = 100; int height = 200; assertThat(Util.getBitmapByteSize(width, height, Bitmap.Config.RGBA_F16)) .isEqualTo(width * height * 8); } } ================================================ FILE: library/test/src/test/java/com/bumptech/glide/util/ViewPreloadSizeProviderTest.java ================================================ package com.bumptech.glide.util; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; import android.view.View; import android.view.ViewGroup; import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(sdk = com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK) public class ViewPreloadSizeProviderTest { private View view; private ViewPreloadSizeProvider provider; @Before public void setUp() { view = new View(ApplicationProvider.getApplicationContext()); provider = new ViewPreloadSizeProvider<>(); } @Test public void testReturnsNullFromGetPreloadSizeBeforeHasSize() { assertNull(provider.getPreloadSize(new Object(), 0, 0)); } @Test public void testReturnsValidSizeFromGetPreloadSizeAfterHasSize() { int width = 4123; int height = 342; provider.onSizeReady(width, height); int[] size = provider.getPreloadSize(new Object(), 0, 0); assertThat(size).asList().containsExactly(width, height).inOrder(); } @Test public void testDoesNotObtainSizeFromViewOnceSizeIsSet() { int width = 123; int height = 456; provider.onSizeReady(width, height); view.setLayoutParams(new ViewGroup.LayoutParams(1, 1)); view.layout(0, 0, 1, 1); provider.setView(view); int[] size = provider.getPreloadSize(new Object(), 0, 0); assertThat(size).asList().containsExactly(width, height).inOrder(); } @Test public void testCanObtainFixedSizeFromView() { int width = 123; int height = 456; view.setLayoutParams(new ViewGroup.LayoutParams(width, height)); view.layout(0, 0, width, height); provider.setView(view); int[] size = provider.getPreloadSize(new Object(), 0, 0); assertThat(size).asList().containsExactly(width, height).inOrder(); } @Test public void testIgnoresNewViewIfAlreadyWaitingOnSizeOfAnotherView() { provider.setView(view); View newView = new View(ApplicationProvider.getApplicationContext()); newView.setLayoutParams(new ViewGroup.LayoutParams(100, 100)); provider.setView(newView); assertNull(provider.getPreloadSize(new Object(), 0, 0)); } @Test public void testCanObtainSizeFromViewWhenGivenViewInConstructor() { int width = 100; int height = 200; view.setLayoutParams(new ViewGroup.LayoutParams(width, height)); view.layout(0, 0, width, height); provider = new ViewPreloadSizeProvider<>(view); int[] size = provider.getPreloadSize(new Object(), 0, 0); assertThat(size).asList().containsExactly(width, height).inOrder(); } } ================================================ FILE: library/test/src/test/java/opengles/GL.java ================================================ package javax.microedition.khronos.opengles; /** * TODO: Figure out why this is necessary and remove it. See: * https://github.com/robolectric/robolectric-gradle-plugin/issues/145 */ public interface GL {} ================================================ FILE: library/test/src/test/resources/org.robolectric.Config.properties ================================================ # Exists only to "enable" logging for test coverage. # TODO: when we can ignore Log.* via configuration, remove this line. shadows=com.bumptech.glide.tests.GlideShadowLog ================================================ FILE: mocks/build.gradle.kts ================================================ plugins { id("com.android.library") } android { namespace = "com.bumptech.glide.mocks" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) implementation(libs.androidx.annotation) implementation(libs.guava) implementation(libs.mockito.core) } apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts") ================================================ FILE: mocks/gradle.properties ================================================ POM_NAME=Glide mocks POM_ARTIFACT_ID=mocks POM_PACKAGING=aar POM_DESCRIPTION=A set of mocks to ease testing with Glide JAR_PREFIX=glide- JAR_POSTFIX= ================================================ FILE: mocks/lint.xml ================================================ ================================================ FILE: mocks/src/main/java/com/bumptech/glide/load/engine/executor/MockGlideExecutor.java ================================================ package com.bumptech.glide.load.engine.executor; import android.os.StrictMode; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.google.common.util.concurrent.ForwardingExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; /** Creates mock {@link GlideExecutor}s. */ @VisibleForTesting public final class MockGlideExecutor { private MockGlideExecutor() { // Utility class. } // Public API. @SuppressWarnings("WeakerAccess") public static GlideExecutor newTestExecutor(ExecutorService executorService) { return new GlideExecutor(executorService); } public static GlideExecutor newMainThreadExecutor() { return newTestExecutor(new DirectExecutorService()); } /** * @deprecated Use {@link #newMainThreadExecutor} instead. */ @Deprecated public static GlideExecutor newMainThreadUnlimitedExecutor() { return newMainThreadExecutor(); } /** * DirectExecutorService that enforces StrictMode and converts ExecutionExceptions into * RuntimeExceptions. */ private static final class DirectExecutorService extends ForwardingExecutorService { private static final StrictMode.ThreadPolicy THREAD_POLICY = new StrictMode.ThreadPolicy.Builder().detectNetwork().penaltyDeath().build(); private final ExecutorService delegate; DirectExecutorService() { delegate = MoreExecutors.newDirectExecutorService(); } @Override protected ExecutorService delegate() { return delegate; } @NonNull @Override public Future submit(@NonNull Runnable task, @NonNull T result) { return getUninterruptibly(super.submit(task, result)); } @NonNull @Override public Future submit(@NonNull Callable task) { return getUninterruptibly(super.submit(task)); } @NonNull @Override public Future submit(@NonNull Runnable task) { return getUninterruptibly(super.submit(task)); } @Override public void execute(@NonNull final Runnable command) { delegate.execute( new Runnable() { @Override public void run() { StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); StrictMode.setThreadPolicy(THREAD_POLICY); try { command.run(); } finally { StrictMode.setThreadPolicy(oldPolicy); } } }); } private Future getUninterruptibly(Future future) { boolean interrupted = false; try { while (!future.isDone()) { try { future.get(); } catch (ExecutionException e) { throw new RuntimeException(e); } catch (InterruptedException e) { interrupted = true; } } } finally { if (interrupted) { Thread.currentThread().interrupt(); } } return future; } } } ================================================ FILE: mocks/src/main/java/com/bumptech/glide/mocks/AnswerSelf.java ================================================ package com.bumptech.glide.mocks; import static org.mockito.Mockito.RETURNS_DEFAULTS; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; /** * Useful default when mocking {@link com.bumptech.glide.request.RequestOptions} or {@link * com.bumptech.glide.RequestBuilder}. * * @param The type of the options and/or builder. */ final class AnswerSelf implements Answer { @SuppressWarnings("unchecked") @Override public T answer(InvocationOnMock invocation) throws Throwable { Object mock = invocation.getMock(); if (invocation.getMethod().getReturnType().isInstance(mock)) { return (T) mock; } else { return (T) RETURNS_DEFAULTS.answer(invocation); } } } ================================================ FILE: mocks/src/main/java/com/bumptech/glide/mocks/MockGlideBuilders.java ================================================ package com.bumptech.glide.mocks; import static org.mockito.Mockito.mock; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.request.RequestOptions; /** * Mocks for various builder patterns in Glide to make testing easier. * *

    All methods share the same behavior. Any method on the builder that returns the builder itself * will default to returning the mock rather than null. Any method on the builder that returns * anything other than the builder will return Mockito's standard default return value. */ public final class MockGlideBuilders { private MockGlideBuilders() {} /** Creates a new {@link RequestBuilder} instance with a matching resource type. */ @SuppressWarnings("unchecked") public static RequestBuilder mockRequestBuilder() { return (RequestBuilder) mockGlideRequest(RequestBuilder.class); } /** Creates a new instance of a generated {@code GlideRequest} class for an application. */ // The suppressions allow callers to get a typed class without warnings in their test code. @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public static > Y mockGlideRequest(Class glideRequest) { return (Y) mock(glideRequest, new AnswerSelf()); } /** Creates a new {@link RequestOptions} instance. */ public static RequestOptions mockRequestOptions() { return mockGlideOptions(RequestOptions.class); } /** Creates a new instance of a generated {@code GlideOptions} class for an application. */ public static T mockGlideOptions(Class glideOptionsClass) { return mock(glideOptionsClass, new AnswerSelf()); } } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ], "semanticCommits": "disabled", "packageRules": [ { "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "automerge": true, "automergeType": "branch" } ] } ================================================ FILE: samples/contacturi/build.gradle.kts ================================================ plugins { id("com.android.application") } android { namespace = "com.bumptech.glide.samples.contacturi" compileSdkVersion = libs.versions.compile.sdk.version.get() defaultConfig { minSdk = libs.versions.min.sdk.version.get().toInt() versionCode = 1 versionName = "1.0" } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } dependencies { implementation(project(":library")) implementation(libs.androidx.appcompat) annotationProcessor(project(":annotation:compiler")) } ================================================ FILE: samples/contacturi/lint.xml ================================================ ================================================ FILE: samples/contacturi/src/main/AndroidManifest.xml ================================================ ================================================ FILE: samples/contacturi/src/main/java/com/bumptech/glide/samples/contacturi/ContactUriModule.java ================================================ package com.bumptech.glide.samples.contacturi; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; /** Ensures that Glide's generated API is created for the Contact Uri sample. */ @GlideModule public class ContactUriModule extends AppGlideModule { // Intentionally empty. } ================================================ FILE: samples/contacturi/src/main/java/com/bumptech/glide/samples/contacturi/MainActivity.java ================================================ package com.bumptech.glide.samples.contacturi; import android.Manifest; import android.app.Activity; import android.content.ContentUris; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; import android.provider.ContactsContract.Contacts; import android.view.View; import android.widget.EditText; import android.widget.ImageView; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.util.Preconditions; /** * An activity that demonstrates loading photos using {@link * com.bumptech.glide.load.data.StreamLocalUriFetcher content uris} through Glide. It works by * making the user to choose a contact when presses a button, and after he chooses a contact with * photo, We try to load both a high res image and thumbnail image of that contact with various * Uris. */ public class MainActivity extends Activity { private static final int REQUEST_CONTACT = 1; private static final int READ_CONTACTS = 0; private ImageView imageViewContact; private ImageView imageViewLookup; private ImageView imageViewPhoto; private ImageView imageViewDisplayPhoto; private EditText numberEntry; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imageViewContact = findViewById(R.id.image_contact); imageViewLookup = findViewById(R.id.image_lookup); imageViewPhoto = findViewById(R.id.image_photo); imageViewDisplayPhoto = findViewById(R.id.image_display_photo); numberEntry = findViewById(R.id.number_entry); // Make sure that user gives application required permissions if (ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // No explanation needed, we can request the permission. ActivityCompat.requestPermissions( this, new String[] {Manifest.permission.READ_CONTACTS}, READ_CONTACTS); } findViewById(R.id.button_pick_contact) .setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI); startActivityForResult(intent, REQUEST_CONTACT); } }); findViewById(R.id.button_find) .setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { Uri uri = Uri.withAppendedPath( ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(numberEntry.getText().toString())); GlideApp.with(MainActivity.this) .load(uri) .override(Target.SIZE_ORIGINAL) .into(imageViewLookup); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CONTACT && resultCode == RESULT_OK) { Uri uri = Preconditions.checkNotNull(data.getData()); final Cursor cursor = getContentResolver().query(uri, null, null, null, null); try { if (cursor != null && cursor.moveToFirst()) { final long contactId = cursor.getLong(cursor.getColumnIndex(Contacts._ID)); showContact(contactId); } } finally { if (cursor != null) { cursor.close(); } } return; } super.onActivityResult(requestCode, resultCode, data); } private void showContact(long id) { GlideRequests glideRequests = GlideApp.with(this); RequestOptions originalSize = new RequestOptions().override(Target.SIZE_ORIGINAL); Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id); glideRequests.load(contactUri).apply(originalSize).into(imageViewContact); Uri lookupUri = Contacts.getLookupUri(getContentResolver(), contactUri); glideRequests.load(lookupUri).apply(originalSize).into(imageViewLookup); Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY); glideRequests.load(photoUri).apply(originalSize).into(imageViewPhoto); Uri displayPhotoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.DISPLAY_PHOTO); glideRequests.load(displayPhotoUri).apply(originalSize).into(imageViewDisplayPhoto); } } ================================================ FILE: samples/contacturi/src/main/res/layout/activity_main.xml ================================================