Skip to content

Commit

Permalink
Store non-SVG icons at a maximum of 512x512 and migrate existing icons
Browse files Browse the repository at this point in the history
  • Loading branch information
alexbakker committed Jan 5, 2025
1 parent 81a26cc commit e59df63
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package com.beemdevelopment.aegis.helpers;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;

import java.io.ByteArrayOutputStream;
import java.util.Objects;

public class BitmapHelper {
private BitmapHelper() {
Expand Down Expand Up @@ -28,4 +35,29 @@ public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {

return Bitmap.createScaledBitmap(bitmap, width, height, true);
}

public static boolean isVaultEntryIconOptimized(VaultEntryIcon icon) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(icon.getBytes(), 0, icon.getBytes().length, opts);
return opts.outWidth <= VaultEntryIcon.MAX_DIMENS && opts.outHeight <= VaultEntryIcon.MAX_DIMENS;
}

public static VaultEntryIcon toVaultEntryIcon(Bitmap bitmap, IconType iconType) {
if (bitmap.getWidth() > VaultEntryIcon.MAX_DIMENS
|| bitmap.getHeight() > VaultEntryIcon.MAX_DIMENS) {
bitmap = resize(bitmap, VaultEntryIcon.MAX_DIMENS, VaultEntryIcon.MAX_DIMENS);
}

ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (Objects.equals(iconType, IconType.PNG)) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
} else {
iconType = IconType.JPEG;
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
}

byte[] data = stream.toByteArray();
return new VaultEntryIcon(data, iconType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SafHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
Expand All @@ -59,7 +61,6 @@
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
Expand All @@ -76,7 +77,6 @@
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
Expand All @@ -103,6 +103,7 @@ public class EditEntryActivity extends AegisActivity {
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private IconPack.Icon _selectedIcon;
private String _pickedMimeType;
private ShapeableImageView _iconView;
private ImageView _saveImageButton;

Expand Down Expand Up @@ -140,8 +141,8 @@ public class EditEntryActivity extends AegisActivity {
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
return;
}
String fileType = SafHelper.getMimeType(this, data.getData());
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
_pickedMimeType = SafHelper.getMimeType(this, data.getData());
if (_pickedMimeType != null && _pickedMimeType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getError() == null) {
Expand Down Expand Up @@ -804,11 +805,12 @@ private VaultEntry parseEntry() throws ParseException {
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();
icon = new VaultEntryIcon(data, IconType.PNG);
IconType iconType = _pickedMimeType == null
? IconType.INVALID : IconType.fromMimeType(_pickedMimeType);
if (iconType == IconType.INVALID) {
iconType = bitmap.hasAlpha() ? IconType.PNG : IconType.JPEG;
}
icon = BitmapHelper.toVaultEntryIcon(bitmap, iconType);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@
import androidx.recyclerview.widget.RecyclerView;

import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.helpers.ViewHelper;
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.google.android.material.dialog.MaterialAlertDialogBuilder;
Expand All @@ -40,8 +44,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

public class ImportEntriesActivity extends AegisActivity {
private View _view;
Expand Down Expand Up @@ -172,7 +178,7 @@ private void processImporterState(DatabaseImporter.State state) {
state.decrypt(this, new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
processDecryptedImporterState(state);
}

@Override
Expand All @@ -187,16 +193,15 @@ public void onCanceled() {
}
});
} else {
importDatabase(state);
processDecryptedImporterState(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
}
}

private void importDatabase(DatabaseImporter.State state) {
List<ImportEntry> importEntries = new ArrayList<>();
private void processDecryptedImporterState(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
Expand All @@ -206,8 +211,29 @@ private void importDatabase(DatabaseImporter.State state) {
return;
}

UUIDMap<VaultEntry> entries = result.getEntries();
for (VaultEntry entry : entries.getValues()) {
Map<UUID, VaultEntryIcon> icons = result.getEntries().getValues().stream()
.filter(e -> e.getIcon() != null
&& !e.getIcon().getType().equals(IconType.SVG)
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
if (!icons.isEmpty()) {
IconOptimizationTask task = new IconOptimizationTask(this, newIcons -> {
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
VaultEntry entry = result.getEntries().getByUUID(mapEntry.getKey());
entry.setIcon(mapEntry.getValue());
}

processImporterResult(result);
});
task.execute(getLifecycle(), icons);
} else {
processImporterResult(result);
}
}

private void processImporterResult(DatabaseImporter.Result result) {
List<ImportEntry> importEntries = new ArrayList<>();
for (VaultEntry entry : result.getEntries().getValues()) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
importEntries.add(importEntry);
Expand Down
38 changes: 37 additions & 1 deletion app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
Expand All @@ -55,12 +58,13 @@
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
Expand Down Expand Up @@ -724,6 +728,37 @@ private void checkTimeSyncSetting() {
}
}

private void checkIconOptimization() {
if (!_vaultManager.getVault().areIconsOptimized()) {
Map<UUID, VaultEntryIcon> oldIcons = _vaultManager.getVault().getEntries().stream()
.filter(e -> e.getIcon() != null
&& !e.getIcon().getType().equals(IconType.SVG)
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));

if (!oldIcons.isEmpty()) {
IconOptimizationTask task = new IconOptimizationTask(this, this::onIconsOptimized);
task.execute(getLifecycle(), oldIcons);
} else {
onIconsOptimized(Collections.emptyMap());
}
}
}

private void onIconsOptimized(Map<UUID, VaultEntryIcon> newIcons) {
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(mapEntry.getKey());
entry.setIcon(mapEntry.getValue());
}

_vaultManager.getVault().setIconsOptimized(true);
saveAndBackupVault();

if (!newIcons.isEmpty()) {
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}

private void onDecryptResult() {
_auditLogRepository.addVaultUnlockedEvent();

Expand Down Expand Up @@ -912,6 +947,7 @@ protected void onStart() {
} else {
loadEntries();
checkTimeSyncSetting();
checkIconOptimization();
}

_lockBackPressHandler.setEnabled(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.beemdevelopment.aegis.ui.tasks;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class IconOptimizationTask extends ProgressDialogTask<Map<UUID, VaultEntryIcon>, Map<UUID, VaultEntryIcon>> {
private final Callback _cb;

public IconOptimizationTask(Context context, Callback cb) {
super(context, context.getString(R.string.optimizing_icon));
_cb = cb;
}

@Override
protected Map<UUID, VaultEntryIcon> doInBackground(Map<UUID, VaultEntryIcon>... params) {
Map<UUID, VaultEntryIcon> res = new HashMap<>();
Context context = getDialog().getContext();

int i = 0;
Map<UUID, VaultEntryIcon> icons = params[0];
for (Map.Entry<UUID, VaultEntryIcon> entry : icons.entrySet()) {
if (icons.size() > 1) {
publishProgress(context.getString(R.string.optimizing_icon_multiple, i + 1, icons.size()));
}
i++;

VaultEntryIcon oldIcon = entry.getValue();
if (oldIcon == null || oldIcon.getType().equals(IconType.SVG)) {
continue;
}
if (BitmapHelper.isVaultEntryIconOptimized(oldIcon)) {
continue;
}

Bitmap bitmap = BitmapFactory.decodeByteArray(oldIcon.getBytes(), 0, oldIcon.getBytes().length);
VaultEntryIcon newIcon = BitmapHelper.toVaultEntryIcon(bitmap, oldIcon.getType());
bitmap.recycle();
res.put(entry.getKey(), newIcon);
}

return res;
}

@Override
protected void onPostExecute(Map<UUID, VaultEntryIcon> results) {
super.onPostExecute(results);
_cb.onTaskFinished(results);
}

public interface Callback {
void onTaskFinished(Map<UUID, VaultEntryIcon> results);
}
}
14 changes: 14 additions & 0 deletions app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class Vault {
private static final int VERSION = 3;
private final UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private final UUIDMap<VaultGroup> _groups = new UUIDMap<>();
private boolean _iconsOptimized = true;

// Whether we've migrated the group list to the new format while parsing the vault
private boolean _isGroupsMigrationFresh = false;
Expand Down Expand Up @@ -42,6 +43,7 @@ public JSONObject toJson(@Nullable EntryFilter filter) {
obj.put("version", VERSION);
obj.put("entries", entriesArray);
obj.put("groups", groupsArray);
obj.put("icons_optimized", _iconsOptimized);

return obj;
} catch (JSONException e) {
Expand Down Expand Up @@ -86,6 +88,10 @@ public static Vault fromJson(JSONObject obj) throws VaultException {

entries.add(entry);
}

if (!obj.optBoolean("icons_optimized")) {
vault.setIconsOptimized(false);
}
} catch (VaultEntryException | JSONException e) {
throw new VaultException(e);
}
Expand All @@ -101,6 +107,14 @@ public boolean isGroupsMigrationFresh() {
return _isGroupsMigrationFresh;
}

public void setIconsOptimized(boolean optimized) {
_iconsOptimized = optimized;
}

public boolean areIconsOptimized() {
return _iconsOptimized;
}

public boolean migrateOldGroup(VaultEntry entry) {
if (entry.getOldGroup() != null) {
Optional<VaultGroup> optGroup = getGroups().getValues()
Expand Down
Loading

0 comments on commit e59df63

Please sign in to comment.