Use glide to load and cache icons

This patch adds a dependency to glide to handle the loading and caching of
icons. In my testing it eliminated the lag previously experienced in the main
activity when quickly scrolling through a large list of entries. It does add an
extra 1MB to the APK size, but I think that's acceptable for the amount of
complexity it handles for us.
This commit is contained in:
Alexander Bakker 2019-06-23 10:18:10 +02:00
parent 550e53f225
commit 9c5726cbca
8 changed files with 248 additions and 66 deletions

View file

@ -4,11 +4,8 @@ import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
@ -39,12 +36,14 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.Collator;
@ -54,6 +53,8 @@ import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import de.hdodenhof.circleimageview.CircleImageView;
@ -135,9 +136,12 @@ public class EditEntryActivity extends AegisActivity {
// fill the fields with values if possible
if (_origEntry != null) {
if (_origEntry.hasIcon()) {
byte[] imageBytes = _origEntry.getIcon();
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_iconView.setImageBitmap(bitmap);
Glide.with(this)
.asDrawable()
.load(_origEntry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
@ -387,28 +391,33 @@ public class EditEntryActivity extends AegisActivity {
setResult(RESULT_OK, intent);
finish();
}
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
Uri inputFile = (data.getData());
InputStream inputStream;
Bitmap bitmap;
try {
inputStream = getContentResolver().openInputStream(inputFile);
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
_kropView.setBitmap(bitmap);
_kropView.setVisibility(View.VISIBLE);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Glide.with(this)
.asBitmap()
.load(data.getData())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
_kropView.setBitmap(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
_iconView.setVisibility(View.GONE);
_kropView.setVisibility(View.VISIBLE);
_saveImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = true;
_hasChangedIcon = true;
@ -499,8 +508,9 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) {
if (_hasCustomIcon) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
drawableToBitmap(_iconView.getDrawable()).compress(Bitmap.CompressFormat.JPEG, 100, stream);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data);
} else {
@ -560,27 +570,6 @@ public class EditEntryActivity extends AegisActivity {
return -1;
}
private static Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
final int width = !drawable.getBounds().isEmpty() ? drawable
.getBounds().width() : drawable.getIntrinsicWidth();
final int height = !drawable.getBounds().isEmpty() ? drawable
.getBounds().height() : drawable.getIntrinsicHeight();
final Bitmap bitmap = Bitmap.createBitmap(width <= 0 ? 1 : width,
height <= 0 ? 1 : height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
private static DatabaseEntry cloneEntry(DatabaseEntry entry) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

View file

@ -0,0 +1,21 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import java.nio.ByteBuffer;
@GlideModule
public class AegisGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.prepend(DatabaseEntry.class, ByteBuffer.class, new IconLoader.Factory());
}
}

View file

@ -0,0 +1,76 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
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 java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<DatabaseEntry, ByteBuffer> {
@Override
public LoadData<ByteBuffer> buildLoadData(@NonNull DatabaseEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
}
@Override
public boolean handles(@NonNull DatabaseEntry model) {
return true;
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private DatabaseEntry _model;
private Fetcher(DatabaseEntry model) {
_model = model;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
byte[] bytes = _model.getIcon();
ByteBuffer buf = ByteBuffer.wrap(bytes);
callback.onDataReady(buf);
}
@Override
public void cleanup() {
}
@Override
public void cancel() {
}
@NonNull
@Override
public Class<ByteBuffer> getDataClass() {
return ByteBuffer.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.MEMORY_CACHE;
}
}
public static class Factory implements ModelLoaderFactory<DatabaseEntry, ByteBuffer> {
@NonNull
@Override
public ModelLoader<DatabaseEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new IconLoader();
}
@Override
public void teardown() {
}
}
}

View file

@ -0,0 +1,31 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Key;
import java.security.MessageDigest;
import java.util.UUID;
public class UUIDKey implements Key {
private UUID _uuid;
public UUIDKey(UUID uuid) {
_uuid = uuid;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(_uuid.toString().getBytes(CHARSET));
}
@Override
public boolean equals(Object o) {
return _uuid.equals(o);
}
@Override
public int hashCode() {
return _uuid.hashCode();
}
}

View file

@ -22,9 +22,9 @@ import java.util.UUID;
import androidx.recyclerview.widget.RecyclerView;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private EntryListView _view;
private List<DatabaseEntry> _entries;
private List<DatabaseEntry> _shownEntries;
private static Listener _listener;
private boolean _showAccountName;
private boolean _tapToReveal;
private int _tapToRevealTime;
@ -37,11 +37,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
public EntryAdapter(Listener listener) {
public EntryAdapter(EntryListView view) {
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_holders = new ArrayList<>();
_listener = listener;
_view = view;
}
public void setShowAccountName(boolean showAccountName) {
@ -56,6 +56,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_tapToRevealTime = number;
}
public DatabaseEntry getEntryAt(int position) {
return _shownEntries.get(position);
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
if (isEntryFiltered(entry)) {
@ -232,7 +236,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return;
}
_listener.onEntryDrop(_shownEntries.get(position));
_view.onEntryDrop(_shownEntries.get(position));
}
@Override
@ -243,7 +247,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
// notify the database first
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
Collections.swap(_entries, firstPosition, secondPosition);
@ -260,7 +264,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(_viewMode.getLayoutId(), parent, false);
return new EntryHolder(view);
EntryHolder holder = new EntryHolder(view);
_view.setPreloadView(holder.getIconView());
return holder;
}
@Override
@ -275,6 +281,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress, _tapToReveal);
holder.setTapToRevealTime(_tapToRevealTime);
holder.loadIcon(_view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
@ -283,7 +290,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
if (_tapToReveal && holder.isCodeHidden()) {
holder.revealCode();
} else {
_listener.onEntryClick(_shownEntries.get(position));
_view.onEntryClick(_shownEntries.get(position));
}
}
});
@ -291,7 +298,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public boolean onLongClick(View v) {
int position = holder.getAdapterPosition();
return _listener.onLongEntryClick(_shownEntries.get(position));
return _view.onLongEntryClick(_shownEntries.get(position));
}
});
holder.setOnRefreshClickListener(new View.OnClickListener() {
@ -306,7 +313,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
// notify the listener that the counter has been incremented
// this gives it a chance to save the database
_listener.onEntryChange(entry);
_view.onEntryChange(entry);
// finally, refresh the code in the UI
holder.refreshCode();
@ -331,7 +338,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
holder.setShowProgress(!_isPeriodUniform);
}
_listener.onPeriodUniformityChanged(_isPeriodUniform);
_view.onPeriodUniformityChanged(_isPeriodUniform);
}
public int getUniformPeriod() {

View file

@ -1,7 +1,5 @@
package com.beemdevelopment.aegis.ui.views;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PorterDuff;
import android.os.Handler;
import android.view.View;
@ -17,7 +15,10 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
public class EntryHolder extends RecyclerView.ViewHolder {
@ -84,15 +85,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileName.setText(" - " + entry.getName());
}
if (_entry.hasIcon()) {
byte[] imageBytes = entry.getIcon();
Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_profileDrawable.setImageBitmap(image);
} else {
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
}
// cancel any scheduled hideCode calls
_hiddenHandler.removeCallbacksAndMessages(null);
@ -103,6 +95,24 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
}
public void loadIcon(Fragment fragment) {
if (_entry.hasIcon()) {
Glide.with(fragment)
.asDrawable()
.load(_entry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_profileDrawable);
} else {
TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
}
}
public ImageView getIconView() {
return _profileDrawable;
}
public void setTapToRevealTime(int number) {
_tapToRevealTime = number;
}

View file

@ -16,10 +16,18 @@ import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
@ -34,6 +42,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private ViewPreloadSizeProvider<DatabaseEntry> _preloadSizeProvider;
private PeriodProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
@ -61,8 +70,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
});
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
_recyclerView.setLayoutManager(mLayoutManager);
// set up icon preloading
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
IconPreloadProvider modelProvider = new IconPreloadProvider();
RecyclerViewPreloader<DatabaseEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
_recyclerView.addOnScrollListener(preloader);
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
_recyclerView.setLayoutManager(layoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
touchHelper.attachToRecyclerView(_recyclerView);
@ -87,6 +102,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
return view;
}
public void setPreloadView(View view) {
_preloadSizeProvider.setView(view);
}
public void setGroupFilter(String group, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(group == null);
_adapter.setGroupFilter(group, apply);
@ -253,4 +272,26 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
outRect.bottom = _height;
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<DatabaseEntry> {
@NonNull
@Override
public List<DatabaseEntry> getPreloadItems(int position) {
DatabaseEntry entry = _adapter.getEntryAt(position);
if (!entry.hasIcon()) {
return Collections.emptyList();
}
return Collections.singletonList(entry);
}
@Nullable
@Override
public RequestBuilder getPreloadRequestBuilder(@NonNull DatabaseEntry entry) {
return Glide.with(EntryListView.this)
.asDrawable()
.load(entry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
}
}