Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions library/src/main/java/com/bumptech/glide/RequestBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,19 @@ public <Y extends Target<TranscodeType>> 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 extends Target<TranscodeType>> Y intoFront(@NonNull Y target) {
return into(target, /* targetListener= */ null, Executors.mainThreadExecutorFront());
}

@NonNull
<Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
Expand Down Expand Up @@ -1001,6 +1014,36 @@ public Target<TranscodeType> 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.
*
* <p>Pre-loading is useful for making sure that resources you are going to to want in the near
* future are available quickly.
*
* <p>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<TranscodeType> preloadFront(int width, int height) {
final PreloadTarget<TranscodeType> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -41,6 +44,10 @@ class DecodeJob<R>
Comparable<DecodeJob<?>>,
Poolable {
private static final String TAG = "DecodeJob";
private static final Option<Supplier<Integer>> 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<R> decodeHelper = new DecodeHelper<>();
private final List<Throwable> throwables = new ArrayList<>();
Expand All @@ -49,6 +56,7 @@ class DecodeJob<R>
private final Pools.Pool<DecodeJob<?>> 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;
Expand All @@ -65,6 +73,7 @@ class DecodeJob<R>
private long startFetchTime;
private boolean onlyRetrieveFromCache;
private Object model;
private Supplier<Integer> glideThreadPriorityOverride;

private Thread currentThread;
private Key currentSourceKey;
Expand Down Expand Up @@ -129,6 +138,7 @@ DecodeJob<R> init(
this.order = order;
this.runReason = RunReason.INITIALIZE;
this.model = model;
this.glideThreadPriorityOverride = options.get(GLIDE_THREAD_PRIORITY);
return this;
}

Expand Down Expand Up @@ -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);
Expand All @@ -335,6 +355,7 @@ private void notifyFailed() {

private void notifyComplete(
Resource<R> resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
restoreThreadPriority();
setNotifiedOrThrow();
callback.onResourceReady(resource, dataSource, isLoadedFromAlternateCacheKey);
}
Expand Down Expand Up @@ -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<R> resource = null;
try {
resource = decodeFromData(currentFetcher, currentData, currentDataSource);
Expand Down
12 changes: 12 additions & 0 deletions library/src/main/java/com/bumptech/glide/util/Executors.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions library/src/main/java/com/bumptech/glide/util/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,25 @@ public void testDoesNotThrowWithNullModelWhenRequestIsBuilt() {
getNullModelRequest().into(target);
}

@Test
public void testDoesNotThrowWithNullModelWhenRequestIsBuilt_Front() {
getNullModelRequest().intoFront(target);
}

@Test
public void testAddsNewRequestToRequestTracker() {
getNullModelRequest().into(target);

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);
Expand All @@ -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<Object>) null);
}

@Test(expected = NullPointerException.class)
public void testThrowsIfGivenNullTarget_Front() {
//noinspection ConstantConditions testing if @NonNull is enforced
getNullModelRequest().intoFront((Target<Object>) 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());
Expand All @@ -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<Object> target = mock(Target.class);
Expand All @@ -129,6 +174,18 @@ public void runTest() {
});
}

@Test
public void doesNotThrowIfIntoTargetCalledOnBackgroundThread_Front() throws InterruptedException {
final Target<Object> 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);
Expand All @@ -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);
Expand All @@ -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();
Expand Down
Loading