Back to Repositories

Testing GIF Drawable Implementation in Glide Android Library

This test suite validates the GifDrawable functionality in Glide, focusing on loading and displaying various types of GIF images in Android applications. It covers transparent, opaque, and interlaced GIFs with different sizing and display scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of GIF loading capabilities:
  • Transparent and opaque GIF handling
  • Interlaced GIF processing
  • Original size and downsampled image loading
  • GIF animation state management
  • Integration with Android ImageView components

Implementation Analysis

The testing approach utilizes JUnit4 with AndroidJUnit4 runner for Android-specific testing. It implements concurrent helper utilities for handling asynchronous GIF loading operations and employs systematic test organization with setup/teardown patterns.

Key patterns include permission management for Android window operations and proper resource cleanup.

Technical Details

Testing tools and configuration:
  • JUnit4 test framework
  • AndroidJUnit4 test runner
  • ConcurrencyHelper for async operations
  • TearDownGlide rule for cleanup
  • GrantPermissionRule for Android permissions
  • Custom resource management for test GIFs

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Systematic test case organization
  • Proper resource management and cleanup
  • Comprehensive edge case handling
  • Clear test method naming conventions
  • Efficient async operation handling
  • Platform-specific permission management

bumptech/glide

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);
  }
}