Skip to content

Instantly share code, notes, and snippets.

@wonsuc
Last active March 31, 2021 03:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save wonsuc/7724258a760701eb3622c9afd42db971 to your computer and use it in GitHub Desktop.
Save wonsuc/7724258a760701eb3622c9afd42db971 to your computer and use it in GitHub Desktop.
Downloading progress with FirebaseUI Storage
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.activity.GlideActivity">
<include layout="@layout/view_toolbar" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
tools:listitem="@layout/item_glide" />
</LinearLayout>
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.google.firebase.storage.StorageReference;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Glide module to register {@link com.firebase.ui.storage.images.FirebaseImageLoader}.
* See: http://bumptech.github.io/glide/doc/generatedapi.html
*/
@GlideModule
public class FirebaseAppGlideModule extends AppGlideModule {
public static final String TAG = "FirebaseAppGlideModule";
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
// Register FirebaseImageLoader to handle StorageReference
registry.append(StorageReference.class, InputStream.class, new FirebaseImageLoader.Factory());
}
public interface UIProgressListener {
void onProgress(long bytesRead, long expectedLength);
/**
* Control how often the listener needs an update. 0% and 100% will always be dispatched.
*
* @return in percentage (0.2 = call {@link #onProgress} around every 0.2 percent of progress)
*/
float getGranualityPercentage();
}
public static void forget(String key) {
DispatchingProgressListener.forget(key);
}
public static void expect(String key, UIProgressListener listener) {
Log.i(TAG, "expect:key:" + key);
DispatchingProgressListener.expect(key, listener);
}
interface ResponseProgressListener {
void update(String key, long bytesRead, long contentLength);
}
static class DispatchingProgressListener implements ResponseProgressListener {
public static final String TAG = "DPListener";
private static final Map<String, List<UIProgressListener>> LISTENERS = new HashMap<>();
private static final Map<String, List<Long>> PROGRESSES = new HashMap<>();
private final Handler handler;
DispatchingProgressListener() {
this.handler = new Handler(Looper.getMainLooper());
}
static void forget(String key) {
LISTENERS.remove(key);
PROGRESSES.remove(key);
}
static void expect(String key, UIProgressListener listener) {
List<UIProgressListener> listeners = LISTENERS.get(key);
if (listeners == null) listeners = new ArrayList<>();
if (!listeners.contains(listener)) {
listeners.add(listener);
LISTENERS.put(key, listeners);
}
}
@Override
public void update(String key, final long bytesRead, final long contentLength) {
Log.i(TAG, String.format(
"update:%s: %d/%d = %.2f%%%n",
key,
bytesRead,
contentLength,
(100f * bytesRead) / contentLength)
);
final List<UIProgressListener> listeners = LISTENERS.get(key);
if (listeners == null) {
return;
}
if (contentLength <= bytesRead) {
forget(key);
}
for (final UIProgressListener listener : listeners) {
int index = listeners.indexOf(listener);
if (needsDispatch(key, index, bytesRead, contentLength, listener.getGranualityPercentage())) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onProgress(bytesRead, contentLength);
}
});
}
}
}
private boolean needsDispatch(String key, int index, long current, long total, float granularity) {
if (granularity == 0 || current == 0 || total == current) {
return true;
}
float percent = 100f * current / total;
long currentProgress = (long) (percent / granularity);
List<Long> progresses = PROGRESSES.get(key);
if (progresses == null) progresses = new ArrayList<>();
Long lastProgress = progresses.size() > index ? progresses.get(index) : null;
if (lastProgress == null) {
progresses.add(currentProgress);
PROGRESSES.put(key, progresses);
return true;
} else if (currentProgress != lastProgress) {
progresses.set(index, currentProgress);
PROGRESSES.put(key, progresses);
return true;
} else {
return false;
}
}
}
}
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
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.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.storage.StorageReference;
import com.google.firebase.storage.StreamDownloadTask;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.MessageDigest;
/**
* ModelLoader implementation to download images from FirebaseStorage with Glide.
* <p>
* <p>
* First, register this class in your AppGlideModule:
* <pre>
* {@literal @}Override
* public void registerComponents(Context context, Registry registry) {
* // Register FirebaseImageLoader to handle StorageReference
* registry.append(StorageReference.class, InputStream.class,
* new FirebaseImageLoader.Factory());
* }
* </pre>
* <p>
* <p>
* Then load a StorageReference into an ImageView.
* <pre>
* StorageReference ref = FirebaseStorage.getInstance().getReference().child("myimage");
* ImageView iv = (ImageView) findViewById(R.id.my_image_view);
*
* GlideApp.with(this)
* .load(ref)
* .into(iv);
* </pre>
*/
public class FirebaseImageLoader implements ModelLoader<StorageReference, InputStream> {
private static final String TAG = "FirebaseImageLoader";
/**
* Factory to create {@link FirebaseImageLoader}.
*/
public static class Factory implements ModelLoaderFactory<StorageReference, InputStream> {
@Override
public ModelLoader<StorageReference, InputStream> build(MultiModelLoaderFactory factory) {
return new FirebaseImageLoader();
}
@Override
public void teardown() {
// No-op
}
}
@Nullable
@Override
public LoadData<InputStream> buildLoadData(StorageReference reference,
int height,
int width,
Options options) {
return new LoadData<>(new FirebaseStorageKey(reference), new FirebaseStorageFetcher(reference));
}
@Override
public boolean handles(StorageReference reference) {
return true;
}
private static class FirebaseStorageKey implements Key {
private StorageReference mRef;
public FirebaseStorageKey(StorageReference ref) {
mRef = ref;
}
@Override
public void updateDiskCacheKey(MessageDigest digest) {
digest.update(mRef.getPath().getBytes(Charset.defaultCharset()));
}
}
private static class FirebaseStorageFetcher implements DataFetcher<InputStream> {
public static final String TAG = "FirebaseStorageFetcher";
private StorageReference mRef;
private StreamDownloadTask mStreamTask;
private InputStream mInputStream;
private FirebaseAppGlideModule.ResponseProgressListener mProgressListener;
public FirebaseStorageFetcher(StorageReference ref) {
// Log.i(TAG, "FirebaseStorageFetcher:ref_path:" + ref.getPath());
mRef = ref;
mProgressListener = new FirebaseAppGlideModule.DispatchingProgressListener();
}
@Override
public void loadData(Priority priority, final DataCallback<? super InputStream> callback) {
// Log.i(TAG, "loadData");
mStreamTask = mRef.getStream();
mStreamTask
.addOnSuccessListener(new OnSuccessListener<StreamDownloadTask.TaskSnapshot>() {
@Override
public void onSuccess(StreamDownloadTask.TaskSnapshot snapshot) {
String key = mRef.getPath();
InputStream in = snapshot.getStream();
long length = snapshot.getTotalByteCount();
try {
ProgressInputStream is = new ProgressInputStream(key, in, length);
is.setListener(new ProgressInputStream.StreamProgressListener() {
@Override
public void update(String key, long bytesRead, long contentLength) {
Log.i(TAG, "onSuccess:update:" +
"key:" + key +
"|bytesRead:" + bytesRead +
"|contentLength:" + contentLength);
mProgressListener.update(key, bytesRead, contentLength);
}
});
mInputStream = is;
} catch (IOException e) {
e.printStackTrace();
callback.onLoadFailed(e);
return;
}
callback.onDataReady(mInputStream);
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
callback.onLoadFailed(e);
}
});
}
@Override
public void cleanup() {
// Close stream if possible
if (mInputStream != null) {
try {
mInputStream.close();
mInputStream = null;
} catch (IOException e) {
Log.w(TAG, "Could not close stream", e);
}
}
}
@Override
public void cancel() {
// Cancel task if possible
if (mStreamTask != null && mStreamTask.isInProgress()) {
mStreamTask.cancel();
}
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.REMOTE;
}
}
private static class ProgressInputStream extends InputStream {
private final String key;
private InputStream in;
private long length, sumRead;
private StreamProgressListener listener;
public interface StreamProgressListener {
void update(String key, long bytesRead, long contentLength);
}
public ProgressInputStream(String key, InputStream inputStream, long length) throws IOException {
Log.i(TAG, "ProgressInputStream");
this.key = key;
this.in = inputStream;
this.sumRead = 0;
this.length = length;
}
@Override
public int read(byte[] b) throws IOException {
int readCount = in.read(b);
evaluate(readCount);
return readCount;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int readCount = in.read(b, off, len);
evaluate(readCount);
return readCount;
}
@Override
public long skip(long n) throws IOException {
long skip = in.skip(n);
evaluate(skip);
return skip;
}
@Override
public int read() throws IOException {
int read = in.read();
if (read != -1) {
evaluate(1);
}
return read;
}
public ProgressInputStream setListener(StreamProgressListener listener) {
this.listener = listener;
return this;
}
private void evaluate(long readCount) {
if (readCount != -1) {
sumRead += readCount;
}
notifyListener();
}
private void notifyListener() {
if (listener != null) listener.update(key, sumRead, length);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:innerRadiusRatio="2.3"
android:shape="ring"
android:thickness="3.8sp"
android:useLevel="true">
<solid android:color="#ff0000" />
</shape>
<?xml version="1.0" encoding="utf-8"?><!-- Display indeterminate progress at the beginning and end, see setImageLevel calls inside MyProgressTarget -->
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- keep [1, 9999] range first to optimize lookup, see LevelListDrawable.LevelListState#indexOfLevel -->
<item
android:drawable="@android:drawable/progress_horizontal"
android:maxLevel="9999"
android:minLevel="1" />
<item
android:drawable="@android:drawable/progress_indeterminate_horizontal"
android:maxLevel="0"
android:minLevel="0" />
<item
android:drawable="@android:drawable/progress_indeterminate_horizontal"
android:maxLevel="10000"
android:minLevel="10000" />
</level-list>
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import com.bumptech.glide.request.target.Target;
import com.google.firebase.storage.FirebaseStorage;
import com.google.firebase.storage.StorageReference;
import com.pwdr.panda.R;
import com.pwdr.panda.image.glide.GlideApp;
import com.pwdr.panda.image.glide.ProgressTarget;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
public class GlideActivity extends BaseActivity {
@BindView(R.id.recycler_view)
RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_glide);
ButterKnife.bind(this);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
StorageReference storageRef = FirebaseStorage.getInstance().getReference();
List<StorageReference> imageRefs = Arrays.asList(
storageRef.child("-L201jy5revqsEAg_xE5").child("0cabae62-e2aa-4272-a409-837f88c06e1d.jpg"),
storageRef.child("table.png"),
storageRef.child("RWU.png"),
storageRef.child("RWU.png")
);
mRecyclerView.setAdapter(new ProgressAdapter(imageRefs));
}
private static class ProgressViewHolder extends ViewHolder {
private final Context context;
private final ImageView image;
private final TextView text;
private final ProgressBar progress;
/**
* Cache target because all the views are tied to this view holder.
*/
private final ProgressTarget<Bitmap> target;
ProgressViewHolder(View root) {
super(root);
context = root.getContext();
image = root.findViewById(R.id.image);
text = root.findViewById(R.id.text);
progress = root.findViewById(R.id.progress);
target = new MyProgressTarget<>(context, new BitmapImageViewTarget(image), progress, image, text);
}
void bind(StorageReference imageRef) {
target.setModel(imageRef); // update target's cache
GlideApp.with(context)
.asBitmap()
.placeholder(R.drawable.glide_progress)
.load(imageRef)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.centerCrop()
.into(target)
;
}
}
/**
* Demonstrates 3 different ways of showing the progress:
* <ul>
* <li>Update a full fledged progress bar</li>
* <li>Update a text view to display size/percentage</li>
* <li>Update the placeholder via Drawable.level</li>
* </ul>
* This last one is tricky: the placeholder that Glide sets can be used as a progress drawable
* without any extra Views in the view hierarchy if it supports levels via <code>usesLevel="true"</code>
* or <code>level-list</code>.
*
* @param <Z> automatically match any real Glide target so it can be used flexibly without reimplementing.
*/
@SuppressLint("SetTextI18n") // text set only for debugging
private static class MyProgressTarget<Z> extends ProgressTarget<Z> {
public static final String TAG = "MyProgressTarget";
private final TextView text;
private final ProgressBar progress;
private final ImageView image;
public MyProgressTarget(Context context, Target<Z> target, ProgressBar progress, ImageView image, TextView text) {
super(context, target);
this.progress = progress;
this.image = image;
this.text = text;
}
@Override
public float getGranualityPercentage() {
return 0.1f; // this matches the format string for #text below
}
@Override
protected void onConnecting() {
Log.i(TAG, "onConnecting");
progress.setIndeterminate(true);
progress.setVisibility(View.VISIBLE);
image.setImageLevel(0);
text.setVisibility(View.VISIBLE);
text.setText("connecting");
}
@Override
protected void onDownloading(long bytesRead, long expectedLength) {
Log.i(TAG, "onDownloading:this:" + this);
progress.setIndeterminate(false);
progress.setProgress((int) (100 * bytesRead / expectedLength));
image.setImageLevel((int) (10000 * bytesRead / expectedLength));
text.setText(String.format(Locale.ROOT, "downloading %.2f/%.2f MB %.1f%%",
bytesRead / 1e6, expectedLength / 1e6, 100f * bytesRead / expectedLength));
}
@Override
protected void onDownloaded() {
Log.i(TAG, "onDownloaded");
progress.setIndeterminate(true);
image.setImageLevel(10000);
text.setText("decoding and transforming");
}
@Override
protected void onDelivered() {
Log.i(TAG, "onDelivered");
progress.setVisibility(View.INVISIBLE);
image.setImageLevel(0); // reset ImageView default
text.setVisibility(View.INVISIBLE);
}
}
private static class ProgressAdapter extends Adapter<ProgressViewHolder> {
private final List<StorageReference> models;
public ProgressAdapter(List<StorageReference> models) {
this.models = models;
}
@Override
public ProgressViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_glide, parent, false);
return new ProgressViewHolder(view);
}
@Override
public void onBindViewHolder(ProgressViewHolder holder, int position) {
holder.bind(models.get(position));
}
@Override
public int getItemCount() {
return models.size();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_margin="4dp">
<!-- see interactions with MyProgressTarget.image
scaleType is fitXY because the LevelListDrawable in github_232_progress contains a fixed sized
indeterminate drawable. fitXY stretches everything out so it's screen-wide.
.centerCrop() on the Glide load will load an appropriately resized bitmap, so that won't be stretched. -->
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
tools:ignore="ContentDescription"
tools:src="@drawable/glide_progress" />
<!-- see interactions with MyProgressTarget.text -->
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:background="#60000000"
android:padding="4dp"
android:textColor="#ffffff"
tools:text="progress: ??.? %" />
<!-- see interactions with MyProgressTarget.progress -->
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="top|end"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/glide_circular" />
</FrameLayout>
package com.pwdr.panda.image.glide.targets;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import com.google.firebase.storage.StorageReference;
import com.pwdr.panda.image.glide.FirebaseAppGlideModule;
import com.pwdr.panda.image.glide.GlideApp;
public abstract class ProgressTarget<Z> extends WrappingTarget<Z> implements FirebaseAppGlideModule.UIProgressListener {
public static final String TAG = "ProgressTarget";
private Context mContext;
private StorageReference model;
private boolean ignoreProgress = true;
public ProgressTarget(Context context, Target<Z> target) {
this(context, null, target);
}
public ProgressTarget(Context context, StorageReference model, Target<Z> target) {
super(target);
this.mContext = context;
this.model = model;
}
public final StorageReference getModel() {
return model;
}
public final void setModel(StorageReference model) {
GlideApp.with(mContext).clear(this); // indirectly calls cleanup
this.model = model;
}
/**
* Convert a model into an path string that is used to match up the StorageReference download requests.
*
* @param model return the representation of the given model, DO NOT use {@link #getModel()} inside this method.
* @return a stable path representation of the model, otherwise the progress reporting won't work
*/
protected String toPathString(StorageReference model) {
if (model == null) return null;
return model.getPath();
}
@Override
public float getGranualityPercentage() {
return 1.0f;
}
@Override
public void onProgress(long bytesRead, long expectedLength) {
if (ignoreProgress) {
return;
}
if (expectedLength == Long.MAX_VALUE) {
onConnecting();
} else if (bytesRead == expectedLength) {
onDownloaded();
} else {
onDownloading(bytesRead, expectedLength);
}
}
/**
* Called when the Glide load has started.
* At this time it is not known if the Glide will even go and use the network to fetch the image.
*/
protected abstract void onConnecting();
/**
* Called when there's any progress on the download; not called when loading from cache.
* At this time we know how many bytes have been transferred through the wire.
*/
protected abstract void onDownloading(long bytesRead, long expectedLength);
/**
* Called when the bytes downloaded reach the length reported by the server; not called when loading from cache.
* At this time it is fairly certain, that Glide either finished reading the stream.
* This means that the image was either already decoded or saved the network stream to cache.
* In the latter case there's more work to do: decode the image from cache and transform.
* These cannot be listened to for progress so it's unsure how fast they'll be, best to show indeterminate progress.
*/
protected abstract void onDownloaded();
/**
* Called when the Glide load has finished either by successfully loading the image or failing to load or cancelled.
* In any case the best is to hide/reset any progress displays.
*/
protected abstract void onDelivered();
private void start() {
FirebaseAppGlideModule.expect(toPathString(model), this);
ignoreProgress = false;
onProgress(0, Long.MAX_VALUE);
}
private void cleanup() {
ignoreProgress = true;
StorageReference model = this.model; // save in case it gets modified
onDelivered();
FirebaseAppGlideModule.forget(toPathString(model));
this.model = null;
}
@Override
public void onLoadStarted(Drawable placeholder) {
super.onLoadStarted(placeholder);
start();
}
@Override
public void onResourceReady(Z resource, Transition<? super Z> transition) {
cleanup();
super.onResourceReady(resource, transition);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
cleanup();
super.onLoadFailed(errorDrawable);
}
@Override
public void onLoadCleared(Drawable placeholder) {
cleanup();
super.onLoadCleared(placeholder);
}
}
import android.graphics.drawable.Drawable;
import android.support.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;
public class WrappingTarget<Z> implements Target<Z> {
protected final Target<Z> target;
public WrappingTarget(Target<Z> target) {
this.target = target;
}
@Override
public void getSize(SizeReadyCallback cb) {
target.getSize(cb);
}
@Override
public void removeCallback(SizeReadyCallback cb) {
target.removeCallback(cb);
}
@Override
public void onLoadStarted(Drawable placeholder) {
target.onLoadStarted(placeholder);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
target.onLoadFailed(errorDrawable);
}
@Override
public void onResourceReady(Z resource, Transition<? super Z> transition) {
target.onResourceReady(resource, transition);
}
@Override
public void onLoadCleared(Drawable placeholder) {
target.onLoadCleared(placeholder);
}
@Override
public Request getRequest() {
return target.getRequest();
}
@Override
public void setRequest(Request request) {
target.setRequest(request);
}
@Override
public void onStart() {
target.onStart();
}
@Override
public void onStop() {
target.onStop();
}
@Override
public void onDestroy() {
target.onDestroy();
}
}
@CameraCornet
Copy link

Can someone publish this as an Android Studio project? I'm having trouble tracking down all the dependencies...

@CameraCornet
Copy link

CameraCornet commented Aug 11, 2018

For example, "com.pwdr.panda.image.glide.GlideApp" and FirebaseAppGlideModule are missing?

@wonsuc
Copy link
Author

wonsuc commented Nov 4, 2018

@CameraCornet Sorry I just saw your comment now, if you still need any help please feel free to ask me anytime.

@roconmachine
Copy link

For example, "com.pwdr.panda.image.glide.GlideApp" and FirebaseAppGlideModule are missing?

@wonsuc
Copy link
Author

wonsuc commented Mar 31, 2021

@roconmachine Sorry, I edited gist, CoreAppGlideModule.java to FirebaseAppGlideModule.java.

com.pwdr.panda.image.glide.GlideApp.java will be generated automatically by Glide library, you don't need to create the class manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment