Merge pull request #1591 from alexbakker/resize-icons

Store non-SVG icons at a maximum of 512x512 and migrate existing icons
pull/1599/head
Michael Schättgen 2 months ago committed by GitHub
commit ec92fb2b31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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() {
@ -28,4 +35,29 @@ public class BitmapHelper {
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);
}
}

@ -35,12 +35,14 @@ import com.beemdevelopment.aegis.encoding.Base32;
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;
@ -59,7 +61,6 @@ 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.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
@ -76,7 +77,6 @@ import com.google.android.material.imageview.ShapeableImageView;
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;
@ -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;
@ -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) {
@ -804,11 +805,12 @@ public class EditEntryActivity extends AegisActivity {
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())){

@ -15,17 +15,21 @@ import androidx.recyclerview.widget.LinearLayoutManager;
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;
@ -40,8 +44,10 @@ import java.io.InputStream;
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;
@ -172,7 +178,7 @@ public class ImportEntriesActivity extends AegisActivity {
state.decrypt(this, new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
processDecryptedImporterState(state);
}
@Override
@ -187,7 +193,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
});
} else {
importDatabase(state);
processDecryptedImporterState(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
@ -195,8 +201,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
}
private void importDatabase(DatabaseImporter.State state) {
List<ImportEntry> importEntries = new ArrayList<>();
private void processDecryptedImporterState(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
@ -206,8 +211,29 @@ public class ImportEntriesActivity extends AegisActivity {
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);

@ -44,9 +44,12 @@ import com.beemdevelopment.aegis.GroupPlaceholderType;
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;
@ -55,12 +58,13 @@ import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFrag
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;
@ -724,6 +728,37 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
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();
@ -912,6 +947,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} else {
loadEntries();
checkTimeSyncSetting();
checkIconOptimization();
}
_lockBackPressHandler.setEnabled(

@ -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);
}
}

@ -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;
@ -42,6 +43,7 @@ public class Vault {
obj.put("version", VERSION);
obj.put("entries", entriesArray);
obj.put("groups", groupsArray);
obj.put("icons_optimized", _iconsOptimized);
return obj;
} catch (JSONException e) {
@ -86,6 +88,10 @@ public class Vault {
entries.add(entry);
}
if (!obj.optBoolean("icons_optimized")) {
vault.setIconsOptimized(false);
}
} catch (VaultEntryException | JSONException e) {
throw new VaultException(e);
}
@ -101,6 +107,14 @@ public class Vault {
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()

@ -103,8 +103,14 @@ public class VaultEntry extends UUIDMap.Value {
entry.setOldGroup(JsonUtils.optString(obj, "group"));
}
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
entry.setIcon(icon);
// Silently ignore any errors that occur when trying to parse the icon of an
// entry. This allows us to introduce new icon types in the future (e.g. WebP)
// without breaking compatibility with older versions of Aegis.
try {
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
entry.setIcon(icon);
} catch (VaultEntryIconException ignored) {
}
return entry;
} catch (OtpInfoException | JSONException e) {

@ -23,6 +23,8 @@ public class VaultEntryIcon implements Serializable {
private final byte[] _hash;
private final IconType _type;
public static final int MAX_DIMENS = 512;
public VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type) {
this(bytes, type, generateHash(bytes, type));
}
@ -70,7 +72,7 @@ public class VaultEntryIcon implements Serializable {
}
@Nullable
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryException {
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryIconException {
try {
Object icon = obj.get("icon");
if (icon == JSONObject.NULL) {
@ -80,7 +82,7 @@ public class VaultEntryIcon implements Serializable {
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));
throw new VaultEntryIconException(String.format("Bad icon MIME type: %s", mime));
}
byte[] iconBytes = Base64.decode((String) icon);
@ -92,7 +94,7 @@ public class VaultEntryIcon implements Serializable {
return new VaultEntryIcon(iconBytes, iconType);
} catch (JSONException | EncodingException e) {
throw new VaultEntryException(e);
throw new VaultEntryIconException(e);
}
}

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.vault;
public class VaultEntryIconException extends Exception {
public VaultEntryIconException(Throwable cause) {
super(cause);
}
public VaultEntryIconException(String message) {
super(message);
}
}

@ -333,6 +333,14 @@ public class VaultRepository {
return _vault.isGroupsMigrationFresh();
}
public boolean areIconsOptimized() {
return _vault.areIconsOptimized();
}
public void setIconsOptimized(boolean optimized) {
_vault.setIconsOptimized(optimized);
}
public VaultFileCredentials getCredentials() {
return _creds == null ? null : _creds.clone();
}

@ -236,6 +236,8 @@
<string name="snackbar_authentication_method">Please select an authentication method</string>
<string name="encrypting_vault">Encrypting the vault</string>
<string name="exporting_vault">Exporting the vault</string>
<string name="optimizing_icon">Optimizing icon</string>
<string name="optimizing_icon_multiple">Optimizing icons %1$d/%2$d</string>
<string name="reading_file">Reading file</string>
<string name="requesting_root_access">Requesting root access</string>
<string name="analyzing_qr">Analyzing QR code</string>

Loading…
Cancel
Save