From fe149362c6fe338720e1d4f3c9802faf33be1893 Mon Sep 17 00:00:00 2001 From: Fares Alhassen Date: Wed, 4 Jun 2025 17:08:51 -0700 Subject: [PATCH] ...text exposed to open source public git repo... PiperOrigin-RevId: 767344325 --- .../com/bumptech/glide/RequestBuilder.java | 43 +++++++++ .../bumptech/glide/load/engine/DecodeJob.java | 32 +++++++ .../com/bumptech/glide/util/Executors.java | 12 +++ .../java/com/bumptech/glide/util/Util.java | 8 ++ .../bumptech/glide/RequestBuilderTest.java | 92 +++++++++++++++++++ 5 files changed, 187 insertions(+) diff --git a/library/src/main/java/com/bumptech/glide/RequestBuilder.java b/library/src/main/java/com/bumptech/glide/RequestBuilder.java index cddcff1ff2..7c522af2e8 100644 --- a/library/src/main/java/com/bumptech/glide/RequestBuilder.java +++ b/library/src/main/java/com/bumptech/glide/RequestBuilder.java @@ -818,6 +818,19 @@ 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 intoFront(@NonNull Y target) { + return into(target, /* targetListener= */ null, Executors.mainThreadExecutorFront()); + } + @NonNull > Y into( @NonNull Y target, @@ -1001,6 +1014,36 @@ public Target preload(int width, int 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 preloadFront(int width, int height) { + final PreloadTarget target = PreloadTarget.obtain(requestManager, width, height); + return intoFront(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 diff --git a/library/src/main/java/com/bumptech/glide/load/engine/DecodeJob.java b/library/src/main/java/com/bumptech/glide/load/engine/DecodeJob.java index 76726faf42..c74abb2b38 100644 --- a/library/src/main/java/com/bumptech/glide/load/engine/DecodeJob.java +++ b/library/src/main/java/com/bumptech/glide/load/engine/DecodeJob.java @@ -1,6 +1,7 @@ package com.bumptech.glide.load.engine; import android.os.Build; +import android.os.Process; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.util.Pools; @@ -10,6 +11,7 @@ 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; @@ -25,6 +27,7 @@ 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 @@ -41,6 +44,10 @@ class DecodeJob Comparable>, Poolable { private static final String TAG = "DecodeJob"; + private static final Option> GLIDE_THREAD_PRIORITY = + Option.memory("glide_thread_priority"); + private static final int DEFAULT_THREAD_PRIORITY = + Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE; private final DecodeHelper decodeHelper = new DecodeHelper<>(); private final List throwables = new ArrayList<>(); @@ -49,6 +56,7 @@ class DecodeJob private final Pools.Pool> pool; private final DeferredEncodeManager deferredEncodeManager = new DeferredEncodeManager<>(); private final ReleaseManager releaseManager = new ReleaseManager(); + private final int threadPriority = Process.getThreadPriority(Process.myTid()); private GlideContext glideContext; private Key signature; @@ -65,6 +73,7 @@ class DecodeJob private long startFetchTime; private boolean onlyRetrieveFromCache; private Object model; + private Supplier glideThreadPriorityOverride; private Thread currentThread; private Key currentSourceKey; @@ -129,6 +138,7 @@ DecodeJob init( this.order = order; this.runReason = RunReason.INITIALIZE; this.model = model; + this.glideThreadPriorityOverride = options.get(GLIDE_THREAD_PRIORITY); return this; } @@ -326,7 +336,17 @@ private void runGenerators() { // onDataFetcherReady. } + private void restoreThreadPriority() { + if (glideThreadPriorityOverride != null && glideThreadPriorityOverride.get() != null) { + // Setting to default instead of original priority because threads can run multiple jobs at + // once so if a new job is started before a previous higher priority job completes, that new + // job will start with a higher priority. + Process.setThreadPriority(Process.myTid(), DEFAULT_THREAD_PRIORITY); + } + } + private void notifyFailed() { + restoreThreadPriority(); setNotifiedOrThrow(); GlideException e = new GlideException("Failed to load resource", new ArrayList<>(throwables)); callback.onLoadFailed(e); @@ -335,6 +355,7 @@ private void notifyFailed() { private void notifyComplete( Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) { + restoreThreadPriority(); setNotifiedOrThrow(); callback.onResourceReady(resource, dataSource, isLoadedFromAlternateCacheKey); } @@ -429,6 +450,17 @@ private void decodeFromRetrievedData() { + ", fetcher: " + currentFetcher); } + if (glideThreadPriorityOverride != null && glideThreadPriorityOverride.get() != null) { + Log.d( + "BigDawg", + "Setting thread priority for thread " + + Process.myTid() + + " from " + + threadPriority + + " to " + + glideThreadPriorityOverride.get().intValue()); + Process.setThreadPriority(Process.myTid(), glideThreadPriorityOverride.get().intValue()); + } Resource resource = null; try { resource = decodeFromData(currentFetcher, currentData, currentDataSource); diff --git a/library/src/main/java/com/bumptech/glide/util/Executors.java b/library/src/main/java/com/bumptech/glide/util/Executors.java index 6a73055469..ed194edf41 100644 --- a/library/src/main/java/com/bumptech/glide/util/Executors.java +++ b/library/src/main/java/com/bumptech/glide/util/Executors.java @@ -19,6 +19,13 @@ 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 @@ -32,6 +39,11 @@ 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; diff --git a/library/src/main/java/com/bumptech/glide/util/Util.java b/library/src/main/java/com/bumptech/glide/util/Util.java index 8609581b87..3f44791108 100644 --- a/library/src/main/java/com/bumptech/glide/util/Util.java +++ b/library/src/main/java/com/bumptech/glide/util/Util.java @@ -151,6 +151,14 @@ 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); diff --git a/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java b/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java index ebac2453ca..2f799440cb 100644 --- a/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java +++ b/library/test/src/test/java/com/bumptech/glide/RequestBuilderTest.java @@ -77,6 +77,11 @@ public void testDoesNotThrowWithNullModelWhenRequestIsBuilt() { getNullModelRequest().into(target); } + @Test + public void testDoesNotThrowWithNullModelWhenRequestIsBuilt_Front() { + getNullModelRequest().intoFront(target); + } + @Test public void testAddsNewRequestToRequestTracker() { getNullModelRequest().into(target); @@ -84,6 +89,13 @@ public void testAddsNewRequestToRequestTracker() { verify(requestManager).track(eq(target), isA(Request.class)); } + @Test + public void testAddsNewRequestToRequestTracker_Front() { + getNullModelRequest().intoFront(target); + + verify(requestManager).track(eq(target), isA(Request.class)); + } + @Test public void testRemovesPreviousRequestFromRequestTracker() { Request previous = mock(Request.class); @@ -94,17 +106,38 @@ public void testRemovesPreviousRequestFromRequestTracker() { verify(requestManager).clear(eq(target)); } + @Test + public void testRemovesPreviousRequestFromRequestTracker_Front() { + Request previous = mock(Request.class); + when(target.getRequest()).thenReturn(previous); + + getNullModelRequest().intoFront(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 testThrowsIfGivenNullTarget_Front() { + //noinspection ConstantConditions testing if @NonNull is enforced + getNullModelRequest().intoFront((Target) null); + } + @Test(expected = NullPointerException.class) public void testThrowsIfGivenNullView() { getNullModelRequest().into((ImageView) null); } + @Test(expected = NullPointerException.class) + public void testThrowsIfGivenNullView_Front() { + getNullModelRequest().intoFront((ImageView) null); + } + @Test(expected = RuntimeException.class) public void testThrowsIfIntoViewCalledOnBackgroundThread() throws InterruptedException { final ImageView imageView = new ImageView(ApplicationProvider.getApplicationContext()); @@ -117,6 +150,18 @@ public void runTest() { }); } + @Test(expected = RuntimeException.class) + public void testThrowsIfIntoViewCalledOnBackgroundThread_Front() throws InterruptedException { + final ImageView imageView = new ImageView(ApplicationProvider.getApplicationContext()); + testInBackground( + new BackgroundTester() { + @Override + public void runTest() { + getNullModelRequest().intoFront(imageView); + } + }); + } + @Test public void doesNotThrowIfIntoTargetCalledOnBackgroundThread() throws InterruptedException { final Target target = mock(Target.class); @@ -129,6 +174,18 @@ public void runTest() { }); } + @Test + public void doesNotThrowIfIntoTargetCalledOnBackgroundThread_Front() throws InterruptedException { + final Target target = mock(Target.class); + testInBackground( + new BackgroundTester() { + @Override + public void runTest() { + getNullModelRequest().intoFront(target); + } + }); + } + @Test public void testMultipleRequestListeners() { getNullModelRequest().addListener(listener1).addListener(listener2).into(target); @@ -146,6 +203,23 @@ public void testMultipleRequestListeners() { .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); } + @Test + public void testMultipleRequestListeners_Front() { + getNullModelRequest().addListener(listener1).addListener(listener2).intoFront(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); @@ -164,6 +238,24 @@ public void testListenerApiOverridesListeners() { .onResourceReady(any(), any(), isA(Target.class), isA(DataSource.class), anyBoolean()); } + @Test + public void testListenerApiOverridesListeners_Front() { + getNullModelRequest().addListener(listener1).listener(listener2).intoFront(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();