Skip to content

Commit 9668157

Browse files
committed
Apply the current Theme for Uri and resource ids.
Applying the Theme allows us to respect the Theme, including dark or light mode resources. We're making the same compromise here that we've made for other model types. Specifically we're only applying this behavior if the non-generic version of the method is used, ie the type of the model is known at compile time. We could try to do this at a lower level, but there's some additional risk of confusion about when or why the Theme is or isn't available. While this isn't perfectly consistent, the behavior can at least be well documented. There's also some risk that passing the Context's Resources / Theme classes to a background thread will result in transient memory leaks. I don't immediately see a direct link between either class and the enclosing Context, but it's hard to be certain. Progress towards #3751
1 parent 464002b commit 9668157

File tree

6 files changed

+214
-37
lines changed

6 files changed

+214
-37
lines changed

instrumentation/src/androidTest/java/com/bumptech/glide/DarkModeTest.java

+124-18
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.bumptech.glide.test.ForceDarkOrLightModeActivity;
3333
import com.google.common.base.Function;
3434
import org.junit.Before;
35+
import org.junit.Ignore;
3536
import org.junit.Rule;
3637
import org.junit.Test;
3738
import org.junit.runner.RunWith;
@@ -50,33 +51,54 @@ public void before() {
5051
assumeTrue(VERSION.SDK_INT >= VERSION_CODES.Q);
5152
}
5253

53-
// TODO(judds): The way we handle data loads in the background for resources is not Theme
54-
// compatible. In particular, the theme gets lost when we convert the resource id to a Uri and
55-
// we don't use the user provided theme. While ResourceBitmapDecoder and ResourceDrawableDecoder
56-
// will use the theme, they're not called for most resource ids because those instead go through
57-
// UriLoader, which just calls contentResolver.openStream. This isn't sufficient to use to theme.
58-
// We could:
59-
// 1. Avoid using contentResolver for android resource Uris and use ResourceBitmapDecoder instead.
60-
// 2. #1 but only for non-raw resources which won't be themed
61-
// 3. Always use Theme.getResources().openRawResource, which, despite the name, works find on
62-
// Drawables and takes into account the theme.
63-
// In addition we'd also need to consider just passing through the theme always, rather than only
64-
// when it's specified by the user. Otherwise whether or not we'd obey dark mode would depend on
65-
// the user also providing the theme from the activity. We'd want to try to make sure that doesn't
66-
// leak the Activity.
67-
@Test
68-
public void load_withDarkModeActivity_usesLightModeDrawable() {
54+
@Test
55+
public void load_withDarkModeActivity_useDarkModeDrawable() {
56+
runActivityTest(
57+
darkModeActivity(),
58+
R.raw.dog_dark,
59+
activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
60+
}
61+
62+
@Test
63+
public void load_withDarkModeActivity_afterLoadingWithLightModeActivity_useDarkModeDrawable() {
64+
// Load with light mode first.
65+
runActivityTest(
66+
lightModeActivity(),
67+
R.raw.dog_light,
68+
activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
69+
70+
// Then again with dark mode to make sure that we do not use the cached resource from the
71+
// previous load.
6972
runActivityTest(
7073
darkModeActivity(),
74+
R.raw.dog_dark,
75+
activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
76+
}
77+
78+
@Test
79+
public void load_withDarkModeActivity_afterLoadingWithLightModeActivity_memoryCacheCleared_useDarkModeDrawable() {
80+
// Load with light mode first.
81+
runActivityTest(
82+
lightModeActivity(),
7183
R.raw.dog_light,
7284
activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
85+
86+
// Then again with dark mode to make sure that we do not use the cached resource from the
87+
// previous load.
88+
runActivityTest(
89+
darkModeActivity(),
90+
R.raw.dog_dark,
91+
activity -> {
92+
Glide.get(context).clearMemory();
93+
return Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL);
94+
});
7395
}
7496

7597
@Test
76-
public void load_withDarkModeFragment_usesLightModeDrawable() {
98+
public void load_withDarkModeFragment_usesDarkModeDrawable() {
7799
runFragmentTest(
78100
darkModeActivity(),
79-
R.raw.dog_light,
101+
R.raw.dog_dark,
80102
fragment -> Glide.with(fragment).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
81103
}
82104

@@ -120,6 +142,35 @@ public void loadResourceNameUri_withDarkModeActivity_darkModeTheme_usesDarkModeD
120142
.theme(activity.getTheme()));
121143
}
122144

145+
@Test
146+
public void loadResourceNameUri_withDarkModeActivity_usesDarkModeDrawable() {
147+
runActivityTest(
148+
darkModeActivity(),
149+
R.raw.dog_dark,
150+
activity ->
151+
Glide.with(activity)
152+
.load(newResourceNameUri(activity, R.drawable.dog))
153+
.override(Target.SIZE_ORIGINAL));
154+
}
155+
156+
@Test
157+
public void loadResourceNameUri_withDarkModeActivity_afterLightModeActivity_usesDarkModeDrawable() {
158+
runActivityTest(
159+
lightModeActivity(),
160+
R.raw.dog_light,
161+
activity ->
162+
Glide.with(activity)
163+
.load(newResourceNameUri(activity, R.drawable.dog))
164+
.override(Target.SIZE_ORIGINAL));
165+
runActivityTest(
166+
darkModeActivity(),
167+
R.raw.dog_dark,
168+
activity ->
169+
Glide.with(activity)
170+
.load(newResourceNameUri(activity, R.drawable.dog))
171+
.override(Target.SIZE_ORIGINAL));
172+
}
173+
123174
@Test
124175
public void loadResourceIdUri_withDarkModeActivity_darkModeTheme_usesDarkModeDrawable() {
125176
runActivityTest(
@@ -132,6 +183,17 @@ public void loadResourceIdUri_withDarkModeActivity_darkModeTheme_usesDarkModeDra
132183
.theme(activity.getTheme()));
133184
}
134185

186+
@Test
187+
public void loadResourceIdUri_withDarkModeActivity_usesDarkModeDrawable() {
188+
runActivityTest(
189+
darkModeActivity(),
190+
R.raw.dog_dark,
191+
activity ->
192+
Glide.with(activity)
193+
.load(newResourceIdUri(activity, R.drawable.dog))
194+
.override(Target.SIZE_ORIGINAL));
195+
}
196+
135197
private static Uri newResourceNameUri(Context context, int resourceId) {
136198
Resources resources = context.getResources();
137199
return newResourceUriBuilder(context)
@@ -198,6 +260,28 @@ public void load_withApplicationContext_darkTheme_usesDarkModeDrawable() {
198260
.theme(input.getTheme()));
199261
}
200262

263+
@Ignore("TODO(#3751): Consider how to deal with themes applied for application context loads.")
264+
@Test
265+
public void load_withApplicationContext_lightTheme_thenDarkTheme_usesDarkModeDrawable() {
266+
runActivityTest(
267+
lightModeActivity(),
268+
R.raw.dog_light,
269+
input ->
270+
Glide.with(input.getApplicationContext())
271+
.load(R.drawable.dog)
272+
.override(Target.SIZE_ORIGINAL)
273+
.theme(input.getTheme()));
274+
275+
runActivityTest(
276+
darkModeActivity(),
277+
R.raw.dog_dark,
278+
input ->
279+
Glide.with(input.getApplicationContext())
280+
.load(R.drawable.dog)
281+
.override(Target.SIZE_ORIGINAL)
282+
.theme(input.getTheme()));
283+
}
284+
201285
@Test
202286
public void loadResourceNameUri_withApplicationContext_darkTheme_usesDarkModeDrawable() {
203287
runActivityTest(
@@ -210,6 +294,28 @@ public void loadResourceNameUri_withApplicationContext_darkTheme_usesDarkModeDra
210294
.theme(input.getTheme()));
211295
}
212296

297+
@Ignore("TODO(#3751): Consider how to deal with themes applied for application context loads.")
298+
@Test
299+
public void loadResourceNameUri_withApplicationContext_darkTheme_afterLightTheme_usesDarkModeDrawable() {
300+
runActivityTest(
301+
lightModeActivity(),
302+
R.raw.dog_light,
303+
input ->
304+
Glide.with(input.getApplicationContext())
305+
.load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog))
306+
.override(Target.SIZE_ORIGINAL)
307+
.theme(input.getTheme()));
308+
309+
runActivityTest(
310+
darkModeActivity(),
311+
R.raw.dog_dark,
312+
input ->
313+
Glide.with(input.getApplicationContext())
314+
.load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog))
315+
.override(Target.SIZE_ORIGINAL)
316+
.theme(input.getTheme()));
317+
}
318+
213319
@Test
214320
public void loadResourceIdUri_withApplicationContext_darkTheme_usesDarkModeDrawable() {
215321
runActivityTest(

integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/RememberGlidePreloadingDataTest.kt

+9-3
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class RememberGlidePreloadingDataTest {
121121
numberOfItemsToPreload = 1,
122122
fixedVisibleItemCount = 1,
123123
) { data: Int, requestBuilder: RequestBuilder<Drawable> ->
124-
requestBuilder.load(data)
124+
requestBuilder.load(data).removeTheme()
125125
}
126126

127127
TextButton(onClick = ::swapData) { Text(text = "Swap") }
@@ -196,18 +196,24 @@ class RememberGlidePreloadingDataTest {
196196
numberOfItemsToPreload = 1,
197197
fixedVisibleItemCount = 1,
198198
) { model, requestBuilder ->
199-
requestBuilder.load(model)
199+
requestBuilder.load(model).removeTheme()
200200
}
201201
}
202202

203203
private fun assertThatModelIsInMemoryCache(@DrawableRes model: Int){
204204
// Wait for previous async image loads to finish
205205
glideComposeRule.waitForIdle()
206206
val nextPreloadModel: Drawable =
207-
Glide.with(context).load(model).onlyRetrieveFromCache(true).submit().get()
207+
Glide.with(context).load(model).removeTheme().onlyRetrieveFromCache(true).submit().get()
208208
assertThat(nextPreloadModel).isNotNull()
209209
}
210210

211+
// We're loading the same resource across two different Contexts. One is the Context from the
212+
// instrumentation package, the other is the package under test. Each Context has it's own Theme,
213+
// neither of which are equal to each other. So that we can verify an item is loaded into memory,
214+
// we remove the themes from all requests that we need to have matching cache keys.
215+
private fun <T> RequestBuilder<T>.removeTheme() = theme(null)
216+
211217
private companion object {
212218
const val model = android.R.drawable.star_big_on
213219

library/src/main/java/com/bumptech/glide/RequestBuilder.java

+46-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package com.bumptech.glide;
22

33
import static com.bumptech.glide.request.RequestOptions.diskCacheStrategyOf;
4-
import static com.bumptech.glide.request.RequestOptions.signatureOf;
54
import static com.bumptech.glide.request.RequestOptions.skipMemoryCacheOf;
65

76
import android.annotation.SuppressLint;
7+
import android.content.ContentResolver;
88
import android.content.Context;
9+
import android.content.res.Resources.Theme;
910
import android.graphics.Bitmap;
1011
import android.graphics.drawable.Drawable;
1112
import android.net.Uri;
@@ -603,6 +604,11 @@ public RequestBuilder<TranscodeType> load(@Nullable Drawable drawable) {
603604
* com.bumptech.glide.load.engine.DiskCacheStrategy#NONE} and/or {@link
604605
* com.bumptech.glide.request.RequestOptions#skipMemoryCache(boolean)} may be appropriate.
605606
*
607+
* <p>If {@code string} is in fact a resource {@link Uri}, you should first parse it to a Uri
608+
* using {@link Uri#parse(String)} and then pass the {@code Uri} to {@link #load(Uri)}. Doing so
609+
* will ensure that we respect the appropriate theme / dark / light mode. As an alternative, you
610+
* can also manually apply the current {@link Theme} using {@link #theme(Theme)}.
611+
*
606612
* @see #load(Object)
607613
* @param string A file path, or a uri or url handled by {@link
608614
* com.bumptech.glide.load.model.UriLoader}.
@@ -624,7 +630,21 @@ public RequestBuilder<TranscodeType> load(@Nullable String string) {
624630
* signature you create based on the data at the given Uri that will invalidate the cache if that
625631
* data changes. Alternatively, using {@link
626632
* com.bumptech.glide.load.engine.DiskCacheStrategy#NONE} and/or {@link
627-
* com.bumptech.glide.request.RequestOptions#skipMemoryCache(boolean)} may be appropriate.
633+
* com.bumptech.glide.request.RequestOptions#skipMemoryCache(boolean)} may be appropriate. The
634+
* only exception to this is that if we recognize the given {@code uri} as having {@link
635+
* ContentResolver#SCHEME_ANDROID_RESOURCE}, then we'll apply {@link AndroidResourceSignature}
636+
* automatically. If we do so, calls to other {@code load()} methods will <em>not</em> override
637+
* the automatically applied signature.
638+
*
639+
* <p>If {@code uri} has a {@link Uri#getScheme()} of {@link
640+
* android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}, then this method will add the
641+
* {@link android.content.res.Resources.Theme} of the {@link Context} associated with this
642+
* {@code requestBuilder} so that we can respect themeable attributes and/or light / dark mode.
643+
* Any call to {@link #theme(Theme)} prior to this method call will be overridden. To avoid this,
644+
* call {@link #theme(Theme)} after calling this method with either {@code null} or the
645+
* {@code Theme} you'd prefer to use instead. Note that even if you change the
646+
* theme, the {@link AndroidResourceSignature} will still be based on the {@link Context}
647+
* theme.
628648
*
629649
* @see #load(Object)
630650
* @param uri The Uri representing the image. Must be of a type handled by {@link
@@ -634,7 +654,22 @@ public RequestBuilder<TranscodeType> load(@Nullable String string) {
634654
@CheckResult
635655
@Override
636656
public RequestBuilder<TranscodeType> load(@Nullable Uri uri) {
637-
return loadGeneric(uri);
657+
return maybeApplyOptionsResourceUri(uri, loadGeneric(uri));
658+
}
659+
660+
private RequestBuilder<TranscodeType> maybeApplyOptionsResourceUri(
661+
@Nullable Uri uri, RequestBuilder<TranscodeType> requestBuilder) {
662+
if (uri == null || !ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) {
663+
return requestBuilder;
664+
}
665+
return applyResourceThemeAndSignature(requestBuilder);
666+
}
667+
668+
private RequestBuilder<TranscodeType> applyResourceThemeAndSignature(
669+
RequestBuilder<TranscodeType> requestBuilder) {
670+
return requestBuilder
671+
.theme(context.getTheme())
672+
.signature(AndroidResourceSignature.obtain(context));
638673
}
639674

640675
/**
@@ -688,14 +723,21 @@ public RequestBuilder<TranscodeType> load(@Nullable File file) {
688723
* method, especially in conjunction with {@link com.bumptech.glide.load.Transformation}s with
689724
* caution for non-{@link Bitmap} {@link Drawable}s.
690725
*
726+
* <p>This method will add the {@link android.content.res.Resources.Theme} of the {@link Context}
727+
* associated with this {@code requestBuilder} so that we can respect themeable attributes and/or
728+
* light / dark mode. Any call to {@link #theme(Theme)} prior to this method call will be
729+
* overridden. To avoid this, call {@link #theme(Theme)} after calling this method with either
730+
* {@code null} or the {@code Theme} you'd prefer to use instead. Note that even if you change the
731+
* theme, the {@link AndroidResourceSignature} will still be based on the {@link Context} theme.
732+
*
691733
* @see #load(Integer)
692734
* @see com.bumptech.glide.signature.AndroidResourceSignature
693735
*/
694736
@NonNull
695737
@CheckResult
696738
@Override
697739
public RequestBuilder<TranscodeType> load(@RawRes @DrawableRes @Nullable Integer resourceId) {
698-
return loadGeneric(resourceId).apply(signatureOf(AndroidResourceSignature.obtain(context)));
740+
return applyResourceThemeAndSignature(loadGeneric(resourceId));
699741
}
700742

701743
/**

library/src/main/java/com/bumptech/glide/load/Options.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ public void putAll(@NonNull Options other) {
1515
values.putAll((SimpleArrayMap<Option<?>, Object>) other.values);
1616
}
1717

18-
// TODO(b/234614365): Allow nullability.
1918
@NonNull
2019
public <T> Options set(@NonNull Option<T> option, @NonNull T value) {
2120
values.put(option, value);
2221
return this;
2322
}
2423

24+
// TODO(b/234614365): Expand usage of this method in BaseRequestOptions so that it's usable for
25+
// other options.
26+
public Options remove(@NonNull Option<?> option) {
27+
values.remove(option);
28+
return this;
29+
}
30+
2531
@Nullable
2632
@SuppressWarnings("unchecked")
2733
public <T> T get(@NonNull Option<T> option) {

library/src/main/java/com/bumptech/glide/load/resource/drawable/ResourceDrawableDecoder.java

+13-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import android.content.res.Resources.Theme;
88
import android.graphics.drawable.Drawable;
99
import android.net.Uri;
10+
import android.text.TextUtils;
1011
import androidx.annotation.DrawableRes;
1112
import androidx.annotation.NonNull;
1213
import androidx.annotation.Nullable;
@@ -59,21 +60,26 @@ public ResourceDrawableDecoder(Context context) {
5960

6061
@Override
6162
public boolean handles(@NonNull Uri source, @NonNull Options options) {
62-
return source.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE);
63+
String scheme = source.getScheme();
64+
return scheme != null && scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE);
6365
}
6466

6567
@Nullable
6668
@Override
6769
public Resource<Drawable> decode(
6870
@NonNull Uri source, int width, int height, @NonNull Options options) {
6971
String packageName = source.getAuthority();
72+
if (TextUtils.isEmpty(packageName)) {
73+
throw new IllegalStateException("Package name for " + source + " is null or empty");
74+
}
7075
Context targetContext = findContextForPackage(source, packageName);
7176
@DrawableRes int resId = findResourceIdFromUri(targetContext, source);
72-
// We can't get a theme from another application.
73-
Theme theme = options.get(THEME);
74-
Preconditions.checkArgument(
75-
targetContext.getPackageName().equals(packageName) || theme == null,
76-
"Can't get a theme from another package");
77+
// Only use the provided theme if we're loading resources from our package. We can't get themes
78+
// from other packages and we don't want to use a theme from our package when loading another
79+
// package's resources.
80+
Theme theme =
81+
Preconditions.checkNotNull(packageName).equals(context.getPackageName())
82+
? options.get(THEME) : null;
7783
Drawable drawable =
7884
theme == null
7985
? DrawableDecoderCompat.getDrawable(context, targetContext, resId)
@@ -82,7 +88,7 @@ public Resource<Drawable> decode(
8288
}
8389

8490
@NonNull
85-
private Context findContextForPackage(Uri source, String packageName) {
91+
private Context findContextForPackage(Uri source, @NonNull String packageName) {
8692
// Fast path
8793
if (packageName.equals(context.getPackageName())) {
8894
return context;

0 commit comments

Comments
 (0)