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

View File

@ -37,7 +37,6 @@ import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.SafHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
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.ui.dialogs.Dialogs;
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.tasks.ImportFileTask;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.bumptech.glide.Glide;
@ -239,19 +239,9 @@ public class EditEntryActivity extends AegisActivity {
_advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible
GlideHelper.loadEntryIcon(Glide.with(this), _origEntry, _iconView);
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;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
@ -548,14 +538,7 @@ public class EditEntryActivity extends AegisActivity {
_hasCustomIcon = true;
_hasChangedIcon = true;
IconViewHelper.setLayerType(_iconView, icon.getIconType());
Glide.with(EditEntryActivity.this)
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
GlideHelper.loadIcon(Glide.with(EditEntryActivity.this), icon, _iconView);
}
private void startEditingIcon(Uri data) {
@ -743,13 +726,14 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) {
if (_hasCustomIcon) {
VaultEntryIcon icon;
if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// the quality parameter is ignored for PNG
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data, IconType.PNG);
icon = new VaultEntryIcon(data, IconType.PNG);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
@ -757,11 +741,12 @@ public class EditEntryActivity extends AegisActivity {
} catch (IOException e) {
throw new ParseException(e.getMessage());
}
entry.setIcon(iconBytes, _selectedIcon.getIconType());
icon = new VaultEntryIcon(iconBytes, _selectedIcon.getIconType());
}
entry.setIcon(icon);
} 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.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.IconRecyclerView;
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 com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
@ -76,12 +75,9 @@ public class IconPickerDialog {
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
return Glide.with(dialog.getContext())
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
RequestBuilder<Drawable> rb = Glide.with(dialog.getContext())
.load(icon.getFile());
return GlideHelper.setCommonOptions(rb, icon.getIconType());
}
}

View File

@ -6,6 +6,7 @@ import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
@ -19,7 +20,7 @@ import java.nio.ByteBuffer;
public class AegisGlideModule extends AppGlideModule {
@Override
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())
.append(InputStream.class, SVG.class, new SvgDecoder())
.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
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(

View File

@ -2,30 +2,30 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.load.Key;
import java.security.MessageDigest;
import java.util.UUID;
public class UUIDKey implements Key {
private UUID _uuid;
public class VaultEntryIconKey implements Key {
private final VaultEntryIcon _icon;
public UUIDKey(UUID uuid) {
_uuid = uuid;
public VaultEntryIconKey(VaultEntryIcon icon) {
_icon = icon;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(_uuid.toString().getBytes(CHARSET));
messageDigest.update(_icon.getHash());
}
@Override
public boolean equals(Object o) {
return _uuid.equals(o);
return _icon.equals(o);
}
@Override
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 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.load.DataSource;
import com.bumptech.glide.load.Option;
@ -15,29 +15,29 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
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);
@Override
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntryIcon icon, int width, int height, @NonNull Options options) {
return new LoadData<>(new VaultEntryIconKey(icon), new Fetcher(icon));
}
@Override
public boolean handles(@NonNull VaultEntry model) {
public boolean handles(@NonNull VaultEntryIcon icon) {
return true;
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private final VaultEntry _model;
private final VaultEntryIcon _icon;
private Fetcher(VaultEntry model) {
_model = model;
private Fetcher(VaultEntryIcon icon) {
_icon = icon;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
byte[] bytes = _model.getIcon();
byte[] bytes = _icon.getBytes();
ByteBuffer buf = ByteBuffer.wrap(bytes);
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
@Override
public ModelLoader<VaultEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new IconLoader();
public ModelLoader<VaultEntryIcon, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new VaultEntryIconLoader();
}
@Override

View File

@ -6,15 +6,10 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class AssignIconHolder extends RecyclerView.ViewHolder implements AssignIconEntry.Listener {
private View _view;
@ -44,37 +39,15 @@ public class AssignIconHolder extends RecyclerView.ViewHolder implements AssignI
_issuer.setText(entry.getEntry().getIssuer());
_accountName.setText(entry.getEntry().getName());
if(!entry.getEntry().hasIcon()) {
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);
}
GlideHelper.loadEntryIcon(Glide.with(_view.getContext()), _entry.getEntry(), _oldIcon);
setNewIcon();
}
private void setNewIcon() {
if (_entry.getNewIcon() != null) {
Glide.with(_view.getContext())
.asDrawable()
.load(_entry.getNewIcon().getFile())
.set(IconLoader.ICON_TYPE, _entry.getNewIcon().getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_newIcon);
GlideHelper.loadIcon(Glide.with(_view.getContext()), _entry.getNewIcon(), _newIcon);
} else {
Glide.with(_view.getContext())
.asDrawable()
.load(R.drawable.ic_icon_unselected)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_newIcon);
GlideHelper.loadResource(Glide.with(_view.getContext()), R.drawable.ic_icon_unselected, _newIcon);
}
_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.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
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.TotpInfo;
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.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f;
@ -197,19 +193,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void loadIcon(Fragment fragment) {
if (_entry.hasIcon()) {
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);
}
GlideHelper.loadEntryIcon(Glide.with(fragment), _entry, _profileDrawable);
}
public ImageView getIconView() {

View File

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

View File

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

View File

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