Use the hash of entry icons as keys for Glide caching

This is mostly a cleanup of the way we do Glide in-memory caching. It
also fixes a few minor issues along the way:

- Entry icon cache keys were based on entry UUID's. This could cause
  problems when changing an entry's icon.
- A TextDrawable could get replaced by the icon of a different entry
  when scrolling through the entry list quickly.
This commit is contained in:
Alexander Bakker 2023-09-19 20:19:15 +02:00
parent 566bcac3e0
commit f1c9c6c5fc
15 changed files with 327 additions and 204 deletions

View file

@ -1,26 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Build;
import android.widget.ImageView;
import com.beemdevelopment.aegis.icons.IconType;
public class IconViewHelper {
private IconViewHelper() {
}
/**
* Sets the layer type of the given ImageView based on the given IconType. If the
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
* is set to hardware.
*/
public static void setLayerType(ImageView view, IconType iconType) {
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return;
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
}

View file

@ -8,6 +8,7 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.OnBackPressedCallback; import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -19,19 +20,20 @@ import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.icons.IconPack; import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog; import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.IconLoader; import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry; import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter; import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
import com.beemdevelopment.aegis.ui.views.IconAdapter; import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader; import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider; import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -121,13 +123,14 @@ public class AssignIconsActivity extends AegisActivity implements AssignIconAdap
ArrayList<UUID> uuids = new ArrayList<>(); ArrayList<UUID> uuids = new ArrayList<>();
for (AssignIconEntry selectedEntry : _entries) { for (AssignIconEntry selectedEntry : _entries) {
VaultEntry entry = selectedEntry.getEntry(); VaultEntry entry = selectedEntry.getEntry();
if(selectedEntry.getNewIcon() != null) { if (selectedEntry.getNewIcon() != null) {
byte[] iconBytes; byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(selectedEntry.getNewIcon().getFile())){ try (FileInputStream inStream = new FileInputStream(selectedEntry.getNewIcon().getFile())){
iconBytes = IOUtils.readFile(inStream); iconBytes = IOUtils.readFile(inStream);
} }
entry.setIcon(iconBytes, selectedEntry.getNewIcon().getIconType()); VaultEntryIcon icon = new VaultEntryIcon(iconBytes, selectedEntry.getNewIcon().getIconType());
entry.setIcon(icon);
uuids.add(entry.getUUID()); uuids.add(entry.getUUID());
_vaultManager.getVault().replaceEntry(entry); _vaultManager.getVault().replaceEntry(entry);
@ -223,12 +226,9 @@ public class AssignIconsActivity extends AegisActivity implements AssignIconAdap
@Nullable @Nullable
@Override @Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) { public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
return Glide.with(AssignIconsActivity.this) RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
.asDrawable() .load(entry.getIcon());
.load(entry) return GlideHelper.setCommonOptions(rb, entry.getIcon().getType());
.set(IconLoader.ICON_TYPE, entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
} }
} }
@ -246,12 +246,9 @@ public class AssignIconsActivity extends AegisActivity implements AssignIconAdap
@Nullable @Nullable
@Override @Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) { public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
return Glide.with(AssignIconsActivity.this) RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
.asDrawable() .load(icon.getFile());
.load(icon.getFile()) return GlideHelper.setCommonOptions(rb, icon.getIconType());
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
} }
} }

View file

@ -37,7 +37,6 @@ import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.helpers.AnimationsHelper; import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper; import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper; import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.SafHelper; import com.beemdevelopment.aegis.helpers.SafHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener; import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher; import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
@ -54,13 +53,14 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.otp.YandexInfo; import com.beemdevelopment.aegis.otp.YandexInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog; import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.IconLoader; import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel; import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask; import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.ui.views.IconAdapter; import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner; import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepository;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
@ -239,19 +239,9 @@ public class EditEntryActivity extends AegisActivity {
_advancedSettings = findViewById(R.id.expandableLayout); _advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible // fill the fields with values if possible
GlideHelper.loadEntryIcon(Glide.with(this), _origEntry, _iconView);
if (_origEntry.hasIcon()) { if (_origEntry.hasIcon()) {
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
Glide.with(this)
.asDrawable()
.load(_origEntry.getIcon())
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
_hasCustomIcon = true; _hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
} }
_textName.setText(_origEntry.getName()); _textName.setText(_origEntry.getName());
@ -548,14 +538,7 @@ public class EditEntryActivity extends AegisActivity {
_hasCustomIcon = true; _hasCustomIcon = true;
_hasChangedIcon = true; _hasChangedIcon = true;
IconViewHelper.setLayerType(_iconView, icon.getIconType()); GlideHelper.loadIcon(Glide.with(EditEntryActivity.this), icon, _iconView);
Glide.with(EditEntryActivity.this)
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
} }
private void startEditingIcon(Uri data) { private void startEditingIcon(Uri data) {
@ -743,13 +726,14 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) { if (_hasChangedIcon) {
if (_hasCustomIcon) { if (_hasCustomIcon) {
VaultEntryIcon icon;
if (_selectedIcon == null) { if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap(); Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream(); ByteArrayOutputStream stream = new ByteArrayOutputStream();
// the quality parameter is ignored for PNG // the quality parameter is ignored for PNG
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] data = stream.toByteArray(); byte[] data = stream.toByteArray();
entry.setIcon(data, IconType.PNG); icon = new VaultEntryIcon(data, IconType.PNG);
} else { } else {
byte[] iconBytes; byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){ try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
@ -757,11 +741,12 @@ public class EditEntryActivity extends AegisActivity {
} catch (IOException e) { } catch (IOException e) {
throw new ParseException(e.getMessage()); throw new ParseException(e.getMessage());
} }
icon = new VaultEntryIcon(iconBytes, _selectedIcon.getIconType());
entry.setIcon(iconBytes, _selectedIcon.getIconType());
} }
entry.setIcon(icon);
} else { } else {
entry.setIcon(null, IconType.INVALID); entry.setIcon(null);
} }
} }

View file

@ -18,14 +18,13 @@ import androidx.recyclerview.widget.GridLayoutManager;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack; import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.glide.IconLoader; import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.views.IconAdapter; import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.ui.views.IconRecyclerView; import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader; import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider; import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
@ -76,12 +75,9 @@ public class IconPickerDialog {
@Nullable @Nullable
@Override @Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) { public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
return Glide.with(dialog.getContext()) RequestBuilder<Drawable> rb = Glide.with(dialog.getContext())
.asDrawable() .load(icon.getFile());
.load(icon.getFile()) return GlideHelper.setCommonOptions(rb, icon.getIconType());
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
} }
} }

View file

@ -6,6 +6,7 @@ import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry; import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.annotation.GlideModule;
@ -19,7 +20,7 @@ import java.nio.ByteBuffer;
public class AegisGlideModule extends AppGlideModule { public class AegisGlideModule extends AppGlideModule {
@Override @Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory()); registry.prepend(VaultEntryIcon.class, ByteBuffer.class, new VaultEntryIconLoader.Factory());
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder()) registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder()) .append(InputStream.class, SVG.class, new SvgDecoder())
.append(ByteBuffer.class, SVG.class, new SvgBytesDecoder()); .append(ByteBuffer.class, SVG.class, new SvgBytesDecoder());

View file

@ -0,0 +1,128 @@
package com.beemdevelopment.aegis.ui.glide;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RawRes;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.DrawableImageViewTarget;
import com.bumptech.glide.request.target.Target;
import java.io.File;
public class GlideHelper {
private GlideHelper() {
}
public static void loadIconFile(RequestManager rm, File file, IconType iconType, ImageView targetView) {
load(rm.load(file), iconType, targetView);
}
public static void loadIcon(RequestManager rm, IconPack.Icon icon, ImageView targetView) {
loadIconFile(rm, icon.getFile(), icon.getIconType(), targetView);
}
public static void loadResource(RequestManager rm, @RawRes @DrawableRes @Nullable Integer resourceId, ImageView targetView) {
loadResource(rm, resourceId, null, targetView);
}
public static void loadResource(RequestManager rm, @RawRes @DrawableRes @Nullable Integer resourceId, @Nullable Integer tint, ImageView targetView) {
setCommonOptions(rm.load(resourceId), null)
.listener(new ViewReadyListener<>(view -> {
if (tint != null) {
view.setColorFilter(tint);
}
setLayerType(targetView, IconType.INVALID);
}))
.into(targetView);
}
public static void loadEntryIcon(RequestManager rm, VaultEntry entry, ImageView targetView) {
if (entry.hasIcon()) {
setCommonOptions(rm.load(entry.getIcon()), entry.getIcon().getType()).into(targetView);
} else {
// Clear any pending loads for targetView, so that the TextDrawable
// we're about to display doesn't get overwritten when that pending load finishes
rm.clear(targetView);
setLayerType(targetView, IconType.INVALID);
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), targetView);
targetView.setImageDrawable(drawable);
}
}
private static void load(RequestBuilder<Drawable> rb, IconType iconType, ImageView targetView) {
setCommonOptions(rb, iconType).into(targetView);
}
public static RequestBuilder<Drawable> setCommonOptions(RequestBuilder<Drawable> rb, IconType iconType) {
if (iconType != null) {
rb = rb.set(VaultEntryIconLoader.ICON_TYPE, iconType)
.listener(new ViewReadyListener<>(targetView -> {
targetView.setImageTintList(null);
setLayerType(targetView, iconType);
}));
}
return rb.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
/**
* Sets the layer type of the given ImageView based on the given IconType. If the
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
* is set to hardware.
*/
private static void setLayerType(ImageView view, IconType iconType) {
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return;
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
private static class ViewReadyListener<T> implements RequestListener<T> {
private final Listener<T> _listener;
public ViewReadyListener(Listener<T> listener) {
_listener = listener;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target<T> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(@NonNull T resource, @NonNull Object model, Target<T> target, @NonNull DataSource dataSource, boolean isFirstResource) {
if (target instanceof DrawableImageViewTarget) {
DrawableImageViewTarget viewTarget = (DrawableImageViewTarget) target;
if (_listener != null) {
_listener.onConfigureImageView(viewTarget.getView());
}
}
return false;
}
public interface Listener<T> {
void onConfigureImageView(ImageView targetView);
}
}
}

View file

@ -22,7 +22,7 @@ public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
@Override @Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) { public boolean handles(@NonNull InputStream source, @NonNull Options options) {
return options.get(IconLoader.ICON_TYPE) == IconType.SVG; return options.get(VaultEntryIconLoader.ICON_TYPE) == IconType.SVG;
} }
public Resource<SVG> decode( public Resource<SVG> decode(

View file

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

View file

@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.icons.IconType; import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Priority; import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Option;
@ -15,29 +15,29 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> { public class VaultEntryIconLoader implements ModelLoader<VaultEntryIcon, ByteBuffer> {
public static final Option<IconType> ICON_TYPE = Option.memory("ICON_TYPE", IconType.INVALID); public static final Option<IconType> ICON_TYPE = Option.memory("ICON_TYPE", IconType.INVALID);
@Override @Override
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) { public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntryIcon icon, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model)); return new LoadData<>(new VaultEntryIconKey(icon), new Fetcher(icon));
} }
@Override @Override
public boolean handles(@NonNull VaultEntry model) { public boolean handles(@NonNull VaultEntryIcon icon) {
return true; return true;
} }
public static class Fetcher implements DataFetcher<ByteBuffer> { public static class Fetcher implements DataFetcher<ByteBuffer> {
private final VaultEntry _model; private final VaultEntryIcon _icon;
private Fetcher(VaultEntry model) { private Fetcher(VaultEntryIcon icon) {
_model = model; _icon = icon;
} }
@Override @Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) { public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
byte[] bytes = _model.getIcon(); byte[] bytes = _icon.getBytes();
ByteBuffer buf = ByteBuffer.wrap(bytes); ByteBuffer buf = ByteBuffer.wrap(bytes);
callback.onDataReady(buf); callback.onDataReady(buf);
} }
@ -65,11 +65,11 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
} }
} }
public static class Factory implements ModelLoaderFactory<VaultEntry, ByteBuffer> { public static class Factory implements ModelLoaderFactory<VaultEntryIcon, ByteBuffer> {
@NonNull @NonNull
@Override @Override
public ModelLoader<VaultEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) { public ModelLoader<VaultEntryIcon, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new IconLoader(); return new VaultEntryIconLoader();
} }
@Override @Override

View file

@ -6,15 +6,10 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper; import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry; import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class AssignIconHolder extends RecyclerView.ViewHolder implements AssignIconEntry.Listener { public class AssignIconHolder extends RecyclerView.ViewHolder implements AssignIconEntry.Listener {
private View _view; private View _view;
@ -44,37 +39,15 @@ public class AssignIconHolder extends RecyclerView.ViewHolder implements AssignI
_issuer.setText(entry.getEntry().getIssuer()); _issuer.setText(entry.getEntry().getIssuer());
_accountName.setText(entry.getEntry().getName()); _accountName.setText(entry.getEntry().getName());
if(!entry.getEntry().hasIcon()) { GlideHelper.loadEntryIcon(Glide.with(_view.getContext()), _entry.getEntry(), _oldIcon);
TextDrawable drawable = TextDrawableHelper.generate(entry.getEntry().getIssuer(), entry.getEntry().getName(), _oldIcon);
_oldIcon.setImageDrawable(drawable);
} else {
Glide.with(_view.getContext())
.asDrawable()
.load(entry.getEntry())
.set(IconLoader.ICON_TYPE, entry.getEntry().getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(_oldIcon);
}
setNewIcon(); setNewIcon();
} }
private void setNewIcon() { private void setNewIcon() {
if (_entry.getNewIcon() != null) { if (_entry.getNewIcon() != null) {
Glide.with(_view.getContext()) GlideHelper.loadIcon(Glide.with(_view.getContext()), _entry.getNewIcon(), _newIcon);
.asDrawable()
.load(_entry.getNewIcon().getFile())
.set(IconLoader.ICON_TYPE, _entry.getNewIcon().getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_newIcon);
} else { } else {
Glide.with(_view.getContext()) GlideHelper.loadResource(Glide.with(_view.getContext()), R.drawable.ic_icon_unselected, _newIcon);
.asDrawable()
.load(R.drawable.ic_icon_unselected)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_newIcon);
} }
_btnReset.setVisibility(_entry.getNewIcon() != null ? View.VISIBLE : View.INVISIBLE); _btnReset.setVisibility(_entry.getNewIcon() != null ? View.VISIBLE : View.INVISIBLE);

View file

@ -11,15 +11,12 @@ import android.widget.TextView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.AccountNamePosition; import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ViewMode; import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.AnimationsHelper; import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener; import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper; import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher; import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.HotpInfo; import com.beemdevelopment.aegis.otp.HotpInfo;
@ -28,10 +25,9 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo; import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.otp.YandexInfo; import com.beemdevelopment.aegis.otp.YandexInfo;
import com.beemdevelopment.aegis.ui.glide.IconLoader; import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class EntryHolder extends RecyclerView.ViewHolder { public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f; private static final float DEFAULT_ALPHA = 1.0f;
@ -197,19 +193,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
} }
public void loadIcon(Fragment fragment) { public void loadIcon(Fragment fragment) {
if (_entry.hasIcon()) { GlideHelper.loadEntryIcon(Glide.with(fragment), _entry, _profileDrawable);
IconViewHelper.setLayerType(_profileDrawable, _entry.getIconType());
Glide.with(fragment)
.asDrawable()
.load(_entry.getIcon())
.set(IconLoader.ICON_TYPE, _entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_profileDrawable);
} else {
TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
}
} }
public ImageView getIconView() { public ImageView getIconView() {

View file

@ -23,8 +23,8 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.CopyBehavior;
import com.beemdevelopment.aegis.AccountNamePosition; import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.CopyBehavior;
import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory; import com.beemdevelopment.aegis.SortCategory;
@ -36,7 +36,7 @@ import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher; import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.glide.IconLoader; import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel; import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
@ -45,7 +45,6 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader; import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider; import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.chip.Chip; import com.google.android.material.chip.Chip;
@ -739,18 +738,16 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
if (!entry.hasIcon()) { if (!entry.hasIcon()) {
return Collections.emptyList(); return Collections.emptyList();
} }
return Collections.singletonList(entry); return Collections.singletonList(entry);
} }
@Nullable @Nullable
@Override @Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) { public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
return Glide.with(EntryListView.this) RequestBuilder<Drawable> rb = Glide.with(EntryListView.this)
.asDrawable() .load(entry.getIcon());
.load(entry) return GlideHelper.setCommonOptions(rb, entry.getIcon().getType());
.set(IconLoader.ICON_TYPE, entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
} }
} }
} }

View file

@ -8,13 +8,11 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper; import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.icons.IconPack; import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType; import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.ui.glide.IconLoader; import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.io.File; import java.io.File;
@ -42,18 +40,9 @@ public class IconHolder extends RecyclerView.ViewHolder {
public void loadIcon(Context context) { public void loadIcon(Context context) {
if (_isCustom) { if (_isCustom) {
int tint = ThemeHelper.getThemeColor(R.attr.iconColorPrimary, context.getTheme()); int tint = ThemeHelper.getThemeColor(R.attr.iconColorPrimary, context.getTheme());
_imageView.setColorFilter(tint); GlideHelper.loadResource(Glide.with(context), R.drawable.ic_plus_black_24dp, tint, _imageView);
_imageView.setImageResource(R.drawable.ic_plus_black_24dp);
} else { } else {
_imageView.setImageTintList(null); GlideHelper.loadIconFile(Glide.with(context), _iconFile, _iconType, _imageView);
IconViewHelper.setLayerType(_imageView, _iconType);
Glide.with(context)
.asDrawable()
.load(_iconFile)
.set(IconLoader.ICON_TYPE, _iconType)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_imageView);
} }
} }
} }

View file

@ -1,8 +1,5 @@
package com.beemdevelopment.aegis.vault; package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo; import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.OtpInfoException;
@ -14,7 +11,7 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.Arrays; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.UUID; import java.util.UUID;
@ -23,8 +20,7 @@ public class VaultEntry extends UUIDMap.Value {
private String _name = ""; private String _name = "";
private String _issuer = ""; private String _issuer = "";
private OtpInfo _info; private OtpInfo _info;
private byte[] _icon; private VaultEntryIcon _icon;
private IconType _iconType = IconType.INVALID;
private boolean _isFavorite; private boolean _isFavorite;
private int _usageCount; private int _usageCount;
private String _note = ""; private String _note = "";
@ -61,8 +57,7 @@ public class VaultEntry extends UUIDMap.Value {
obj.put("issuer", _issuer); obj.put("issuer", _issuer);
obj.put("note", _note); obj.put("note", _note);
obj.put("favorite", _isFavorite); obj.put("favorite", _isFavorite);
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon)); VaultEntryIcon.toJson(_icon, obj);
obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType());
obj.put("info", _info.toJson()); obj.put("info", _info.toJson());
JSONArray groupUuids = new JSONArray(); JSONArray groupUuids = new JSONArray();
@ -107,21 +102,11 @@ public class VaultEntry extends UUIDMap.Value {
entry.setOldGroup(JsonUtils.optString(obj, "group")); entry.setOldGroup(JsonUtils.optString(obj, "group"));
} }
Object icon = obj.get("icon"); VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
if (icon != JSONObject.NULL) { entry.setIcon(icon);
String mime = JsonUtils.optString(obj, "icon_mime");
IconType iconType = mime == null ? IconType.JPEG : IconType.fromMimeType(mime);
if (iconType == IconType.INVALID) {
throw new VaultEntryException(String.format("Bad icon MIME type: %s", mime));
}
byte[] iconBytes = Base64.decode((String) icon);
entry.setIcon(iconBytes, iconType);
}
return entry; return entry;
} catch (OtpInfoException | JSONException | EncodingException e) { } catch (OtpInfoException | JSONException e) {
throw new VaultEntryException(e); throw new VaultEntryException(e);
} }
} }
@ -138,14 +123,10 @@ public class VaultEntry extends UUIDMap.Value {
return _groups; return _groups;
} }
public byte[] getIcon() { public VaultEntryIcon getIcon() {
return _icon; return _icon;
} }
public IconType getIconType() {
return _iconType;
}
public OtpInfo getInfo() { public OtpInfo getInfo() {
return _info; return _info;
} }
@ -154,9 +135,15 @@ public class VaultEntry extends UUIDMap.Value {
return _usageCount; return _usageCount;
} }
public String getNote() { return _note; } public String getNote() {
return _note;
}
public boolean isFavorite() { return _isFavorite; }; public boolean isFavorite() {
return _isFavorite;
}
;
public void setName(String name) { public void setName(String name) {
_name = name; _name = name;
@ -188,20 +175,25 @@ public class VaultEntry extends UUIDMap.Value {
_info = info; _info = info;
} }
public void setIcon(byte[] icon, IconType iconType) { public void setIcon(VaultEntryIcon icon) {
_icon = icon; _icon = icon;
_iconType = iconType;
} }
public boolean hasIcon() { public boolean hasIcon() {
return _icon != null; return _icon != null;
} }
public void setUsageCount(int usageCount) { _usageCount = usageCount; } public void setUsageCount(int usageCount) {
_usageCount = usageCount;
}
public void setNote(String note) { _note = note; } public void setNote(String note) {
_note = note;
}
public void setIsFavorite(boolean isFavorite) { _isFavorite = isFavorite; } public void setIsFavorite(boolean isFavorite) {
_isFavorite = isFavorite;
}
void setOldGroup(String oldGroup) { void setOldGroup(String oldGroup) {
_oldGroup = oldGroup; _oldGroup = oldGroup;
@ -230,8 +222,7 @@ public class VaultEntry extends UUIDMap.Value {
return getName().equals(entry.getName()) return getName().equals(entry.getName())
&& getIssuer().equals(entry.getIssuer()) && getIssuer().equals(entry.getIssuer())
&& getInfo().equals(entry.getInfo()) && getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon()) && Objects.equals(getIcon(), entry.getIcon())
&& getIconType().equals(entry.getIconType())
&& getNote().equals(entry.getNote()) && getNote().equals(entry.getNote())
&& isFavorite() == entry.isFavorite() && isFavorite() == entry.isFavorite()
&& getGroups().equals(entry.getGroups()); && getGroups().equals(entry.getGroups());

View file

@ -0,0 +1,108 @@
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.google.common.hash.HashCode;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class VaultEntryIcon implements Serializable {
private final byte[] _bytes;
private final byte[] _hash;
private final IconType _type;
public VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type) {
this(bytes, type, generateHash(bytes, type));
}
VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type, byte @NonNull [] hash) {
_bytes = bytes;
_hash = hash;
_type = type;
}
public byte @NonNull [] getBytes() {
return _bytes;
}
public byte @NonNull [] getHash() {
return _hash;
}
@NonNull
public IconType getType() {
return _type;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof VaultEntryIcon)) {
return false;
}
VaultEntryIcon entry = (VaultEntryIcon) o;
return Arrays.equals(getHash(), entry.getHash());
}
@Override
public int hashCode() {
return HashCode.fromBytes(_hash).asInt();
}
static void toJson(@Nullable VaultEntryIcon icon, @NonNull JSONObject obj) throws JSONException {
obj.put("icon", icon == null ? JSONObject.NULL : Base64.encode(icon.getBytes()));
if (icon != null) {
obj.put("icon_mime", icon.getType().toMimeType());
obj.put("icon_hash", Hex.encode(icon.getHash()));
}
}
@Nullable
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryException {
try {
Object icon = obj.get("icon");
if (icon == JSONObject.NULL) {
return null;
}
String mime = JsonUtils.optString(obj, "icon_mime");
IconType iconType = mime == null ? IconType.JPEG : IconType.fromMimeType(mime);
if (iconType == IconType.INVALID) {
throw new VaultEntryException(String.format("Bad icon MIME type: %s", mime));
}
byte[] iconBytes = Base64.decode((String) icon);
String iconHashStr = JsonUtils.optString(obj, "icon_hash");
if (iconHashStr != null) {
byte[] iconHash = Hex.decode(iconHashStr);
return new VaultEntryIcon(iconBytes, iconType, iconHash);
}
return new VaultEntryIcon(iconBytes, iconType);
} catch (JSONException | EncodingException e) {
throw new VaultEntryException(e);
}
}
private static byte @NonNull [] generateHash(byte @NonNull [] bytes, @NonNull IconType type) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(type.toMimeType().getBytes(StandardCharsets.UTF_8));
return md.digest(bytes);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}