Introduce support for icon packs

This commit is contained in:
Alexander Bakker 2020-07-08 11:18:40 +02:00
parent c977b9a064
commit 4f38988c0d
44 changed files with 2128 additions and 54 deletions

View file

@ -145,6 +145,7 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.github.avito-tech:krop:0.52' implementation 'com.github.avito-tech:krop:0.52'
implementation "com.github.bumptech.glide:annotations:${glideVersion}" implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}" implementation "com.github.bumptech.glide:glide:${glideVersion}"

View file

@ -19,6 +19,7 @@ import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.services.NotificationService; import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity; import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.util.IOUtils;
@ -41,6 +42,7 @@ public class AegisApplication extends Application {
private Preferences _prefs; private Preferences _prefs;
private List<LockListener> _lockListeners; private List<LockListener> _lockListeners;
private boolean _blockAutoLock; private boolean _blockAutoLock;
private IconPackManager _iconPackManager;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel"; private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault"; private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
@ -55,6 +57,7 @@ public class AegisApplication extends Application {
super.onCreate(); super.onCreate();
_prefs = new Preferences(this); _prefs = new Preferences(this);
_lockListeners = new ArrayList<>(); _lockListeners = new ArrayList<>();
_iconPackManager = new IconPackManager(this);
Iconics.init(this); Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic()); Iconics.registerFont(new MaterialDesignIconic());
@ -126,6 +129,10 @@ public class AegisApplication extends Application {
return _manager; return _manager;
} }
public IconPackManager getIconPackManager() {
return _iconPackManager;
}
public Preferences getPreferences() { public Preferences getPreferences() {
return _prefs; return _prefs;
} }

View file

@ -0,0 +1,25 @@
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);
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
}

View file

@ -48,6 +48,6 @@ public class TextDrawableHelper {
.width(view.getLayoutParams().width) .width(view.getLayoutParams().width)
.height(view.getLayoutParams().height) .height(view.getLayoutParams().height)
.endConfig() .endConfig()
.buildRect(text.substring(0, 1).toUpperCase(), color); .buildRound(text.substring(0, 1).toUpperCase(), color);
} }
} }

View file

@ -0,0 +1,183 @@
package com.beemdevelopment.aegis.icons;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Objects;
import com.google.common.io.Files;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPack {
private UUID _uuid;
private String _name;
private int _version;
private List<Icon> _icons;
private File _dir;
private IconPack(UUID uuid, String name, int version, List<Icon> icons) {
_uuid = uuid;
_name = name;
_version = version;
_icons = icons;
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public int getVersion() {
return _version;
}
public List<Icon> getIcons() {
return Collections.unmodifiableList(_icons);
}
/**
* Retrieves a list of icons suggested for the given issuer.
*/
public List<Icon> getSuggestedIcons(String issuer) {
if (issuer == null || issuer.isEmpty()) {
return new ArrayList<>();
}
return _icons.stream()
.filter(i -> i.isSuggestedFor(issuer))
.collect(Collectors.toList());
}
@Nullable
public File getDirectory() {
return _dir;
}
void setDirectory(@NonNull File dir) {
_dir = dir;
}
/**
* Indicates whether some other object is "equal to" this one. The object does not
* necessarily have to be the same instance. Equality of UUID and version will make
* this method return true;
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof IconPack)) {
return false;
}
IconPack pack = (IconPack) o;
return super.equals(pack) || (getUUID().equals(pack.getUUID()) && getVersion() == pack.getVersion());
}
@Override
public int hashCode() {
return Objects.hashCode(_uuid, _version);
}
public static IconPack fromJson(JSONObject obj) throws JSONException {
UUID uuid;
String uuidString = obj.getString("uuid");
try {
uuid = UUID.fromString(uuidString);
} catch (IllegalArgumentException e) {
throw new JSONException(String.format("Bad UUID format: %s", uuidString));
}
String name = obj.getString("name");
int version = obj.getInt("version");
JSONArray array = obj.getJSONArray("icons");
List<Icon> icons = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
Icon icon = Icon.fromJson(array.getJSONObject(i));
icons.add(icon);
}
return new IconPack(uuid, name, version, icons);
}
public static IconPack fromBytes(byte[] data) throws JSONException {
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return IconPack.fromJson(obj);
}
public static class Icon implements Serializable {
private final String _relFilename;
private final String _category;
private final List<String> _issuers;
private File _file;
protected Icon(String filename, String category, List<String> issuers) {
_relFilename = filename;
_category = category;
_issuers = issuers;
}
public String getRelativeFilename() {
return _relFilename;
}
@Nullable
public File getFile() {
return _file;
}
void setFile(@NonNull File file) {
_file = file;
}
public IconType getIconType() {
return IconType.fromFilename(_relFilename);
}
@SuppressWarnings("UnstableApiUsage")
public String getName() {
return Files.getNameWithoutExtension(new File(_relFilename).getName());
}
public String getCategory() {
return _category;
}
public List<String> getIssuers() {
return Collections.unmodifiableList(_issuers);
}
public boolean isSuggestedFor(String issuer) {
return getIssuers().stream()
.anyMatch(is -> is.toLowerCase().contains(issuer.toLowerCase()));
}
public static Icon fromJson(JSONObject obj) throws JSONException {
String filename = obj.getString("filename");
String category = obj.isNull("category") ? null : obj.getString("category");
JSONArray array = obj.getJSONArray("issuer");
List<String> issuers = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
String issuer = array.getString(i);
issuers.add(issuer);
}
return new Icon(filename, category, issuers);
}
}
}

View file

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

View file

@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.icons;
public class IconPackExistsException extends IconPackException {
private IconPack _pack;
public IconPackExistsException(IconPack pack) {
super(String.format("Icon pack %s (%d) already exists", pack.getName(), pack.getVersion()));
_pack = pack;
}
public IconPack getIconPack() {
return _pack;
}
}

View file

@ -0,0 +1,219 @@
package com.beemdevelopment.aegis.icons;
import android.content.Context;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.IOUtils;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import org.json.JSONException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPackManager {
private static final String _packDefFilename = "pack.json";
private File _iconsBaseDir;
private List<IconPack> _iconPacks;
public IconPackManager(Context context) {
_iconPacks = new ArrayList<>();
_iconsBaseDir = new File(context.getFilesDir(), "icons");
rescanIconPacks();
}
private IconPack getIconPackByUUID(UUID uuid) {
List<IconPack> packs = _iconPacks.stream().filter(i -> i.getUUID().equals(uuid)).collect(Collectors.toList());
if (packs.size() == 0) {
return null;
}
return packs.get(0);
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}
public void removeIconPack(IconPack pack) throws IconPackException {
try {
File dir = getIconPackDir(pack);
deleteDir(dir);
} catch (IOException e) {
throw new IconPackException(e);
}
_iconPacks.remove(pack);
}
public IconPack importPack(File inFile) throws IconPackException {
try {
// read and parse the icon pack definition file of the icon pack
ZipFile zipFile = new ZipFile(inFile);
FileHeader packHeader = zipFile.getFileHeader(_packDefFilename);
if (packHeader == null) {
throw new IOException("Unable to find pack.json in the root of the ZIP file");
}
IconPack pack;
byte[] defBytes;
try (ZipInputStream inStream = zipFile.getInputStream(packHeader)) {
defBytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(defBytes);
}
// create a new directory to store the icon pack, based on the UUID and version
File packDir = getIconPackDir(pack);
if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
throw new IOException("Attempted to write outside of the parent directory");
}
if (packDir.exists()) {
throw new IconPackExistsException(pack);
}
IconPack existingPack = getIconPackByUUID(pack.getUUID());
if (existingPack != null) {
throw new IconPackExistsException(existingPack);
}
if (!packDir.exists() && !packDir.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
// extract each of the defined icons to the icon pack directory
for (IconPack.Icon icon : pack.getIcons()) {
File destFile = new File(packDir, icon.getRelativeFilename());
FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
if (iconHeader == null) {
throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
}
// create new directories for this file if needed
File parent = destFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
FileOutputStream outStream = new FileOutputStream(destFile)) {
IOUtils.copy(inStream, outStream);
}
// after successful copy of the icon, store the new filename
icon.setFile(destFile);
}
// write the icon pack definition file to the newly created directory
try (FileOutputStream outStream = new FileOutputStream(new File(packDir, _packDefFilename))) {
outStream.write(defBytes);
}
// after successful extraction of the icon pack, store the new directory
pack.setDirectory(packDir);
_iconPacks.add(pack);
return pack;
} catch (IOException | JSONException e) {
throw new IconPackException(e);
}
}
private void rescanIconPacks() {
_iconPacks.clear();
File[] dirs = _iconsBaseDir.listFiles();
if (dirs == null) {
return;
}
for (File dir : dirs) {
if (!dir.isDirectory()) {
continue;
}
UUID uuid;
try {
uuid = UUID.fromString(dir.getName());
} catch (IllegalArgumentException e) {
e.printStackTrace();
continue;
}
File versionDir = getLatestVersionDir(dir);
if (versionDir != null) {
IconPack pack;
try (FileInputStream inStream = new FileInputStream(new File(versionDir, _packDefFilename))) {
byte[] bytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(bytes);
pack.setDirectory(versionDir);
} catch (JSONException | IOException e) {
e.printStackTrace();
continue;
}
for (IconPack.Icon icon : pack.getIcons()) {
icon.setFile(new File(versionDir, icon.getRelativeFilename()));
}
// do a sanity check on the UUID and version
if (pack.getUUID().equals(uuid) && pack.getVersion() == Integer.parseInt(versionDir.getName())) {
_iconPacks.add(pack);
}
}
}
}
private File getIconPackDir(IconPack pack) {
return new File(_iconsBaseDir, pack.getUUID() + File.separator + pack.getVersion());
}
@Nullable
private static File getLatestVersionDir(File packDir) {
File[] dirs = packDir.listFiles();
if (dirs == null) {
return null;
}
int latestVersion = -1;
for (File versionDir : dirs) {
int version;
try {
version = Integer.parseInt(versionDir.getName());
} catch (NumberFormatException ignored) {
continue;
}
if (latestVersion == -1 || version > latestVersion) {
latestVersion = version;
}
}
if (latestVersion == -1) {
return null;
}
return new File(packDir, Integer.toString(latestVersion));
}
private static void deleteDir(File dir) throws IOException {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
deleteDir(child);
}
}
}
if (!dir.delete()) {
throw new IOException(String.format("Unable to delete directory: %s", dir));
}
}
}

View file

@ -0,0 +1,52 @@
package com.beemdevelopment.aegis.icons;
import com.google.common.io.Files;
public enum IconType {
INVALID,
SVG,
PNG,
JPEG;
public static IconType fromMimeType(String mimeType) {
switch (mimeType) {
case "image/svg+xml":
return SVG;
case "image/png":
return PNG;
case "image/jpeg":
return JPEG;
default:
return INVALID;
}
}
@SuppressWarnings("UnstableApiUsage")
public static IconType fromFilename(String filename) {
switch (Files.getFileExtension(filename).toLowerCase()) {
case "svg":
return SVG;
case "png":
return PNG;
case "jpg":
// intentional fallthrough
case "jpeg":
return JPEG;
default:
return INVALID;
}
}
public String toMimeType() {
switch (this) {
case SVG:
return "image/svg+xml";
case PNG:
return "image/png";
case JPEG:
return "image/jpeg";
default:
throw new RuntimeException(String.format("Can't convert icon type %s to MIME type", this));
}
}
}

View file

@ -5,13 +5,12 @@ import android.content.DialogInterface;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper; import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask; import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
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.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile; import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials; import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException; import com.beemdevelopment.aegis.vault.VaultFileException;
@ -140,7 +139,7 @@ public class AegisImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException { private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try { try {
return VaultEntry.fromJson(obj); return VaultEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | EncodingException e) { } catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString()); throw new DatabaseImporterEntryException(e, obj.toString());
} }
} }

View file

@ -34,7 +34,10 @@ import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException; import com.beemdevelopment.aegis.encoding.EncodingException;
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.TextDrawableHelper; import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo; import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo; import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfo;
@ -42,23 +45,32 @@ 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.ui.dialogs.Dialogs; 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.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner; import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager; import com.beemdevelopment.aegis.vault.VaultManager;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
@ -72,6 +84,7 @@ public class EditEntryActivity extends AegisActivity {
private boolean _hasCustomIcon = false; private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic // keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false; private boolean _hasChangedIcon = false;
private IconPack.Icon _selectedIcon;
private boolean _isEditingIcon; private boolean _isEditingIcon;
private CircleImageView _iconView; private CircleImageView _iconView;
private ImageView _saveImageButton; private ImageView _saveImageButton;
@ -165,9 +178,11 @@ public class EditEntryActivity extends AegisActivity {
// fill the fields with values if possible // fill the fields with values if possible
if (_origEntry.hasIcon()) { if (_origEntry.hasIcon()) {
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
Glide.with(this) Glide.with(this)
.asDrawable() .asDrawable()
.load(_origEntry) .load(_origEntry)
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false) .skipMemoryCache(false)
.into(_iconView); .into(_iconView);
@ -237,7 +252,7 @@ public class EditEntryActivity extends AegisActivity {
}); });
_iconView.setOnClickListener(v -> { _iconView.setOnClickListener(v -> {
startIconSelectionActivity(); startIconSelection();
}); });
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() { _dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@ -384,11 +399,13 @@ public class EditEntryActivity extends AegisActivity {
}); });
break; break;
case R.id.action_edit_icon: case R.id.action_edit_icon:
startIconSelectionActivity(); startIconSelection();
break; break;
case R.id.action_default_icon: case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView); TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable); _iconView.setImageDrawable(drawable);
_selectedIcon = null;
_hasCustomIcon = false; _hasCustomIcon = false;
_hasChangedIcon = true; _hasChangedIcon = true;
default: default:
@ -398,7 +415,7 @@ public class EditEntryActivity extends AegisActivity {
return true; return true;
} }
private void startIconSelectionActivity() { private void startImageSelectionActivity() {
Intent galleryIntent = new Intent(Intent.ACTION_PICK); Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*"); galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
@ -410,6 +427,40 @@ public class EditEntryActivity extends AegisActivity {
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST); startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
} }
private void startIconSelection() {
List<IconPack> iconPacks = getApp().getIconPackManager().getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.collect(Collectors.toList());
if (iconPacks.size() == 0) {
startImageSelectionActivity();
return;
}
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
_selectedIcon = icon;
_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);
}
@Override
public void onCustomSelected() {
startImageSelectionActivity();
}
});
Dialogs.showSecureDialog(dialog);
}
private void startEditingIcon(Uri data) { private void startEditingIcon(Uri data) {
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
@ -438,11 +489,12 @@ public class EditEntryActivity extends AegisActivity {
} }
private void stopEditingIcon(boolean save) { private void stopEditingIcon(boolean save) {
if (save) { if (save && _selectedIcon == null) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap()); _iconView.setImageBitmap(_kropView.getCroppedBitmap());
} }
_iconView.setVisibility(View.VISIBLE); _iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE); _kropView.setVisibility(View.GONE);
_hasCustomIcon = _hasCustomIcon || save; _hasCustomIcon = _hasCustomIcon || save;
_hasChangedIcon = save; _hasChangedIcon = save;
_isEditingIcon = false; _isEditingIcon = false;
@ -577,13 +629,26 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) { if (_hasChangedIcon) {
if (_hasCustomIcon) { if (_hasCustomIcon) {
if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap(); Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream(); ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); // the quality parameter is ignored for PNG
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] data = stream.toByteArray(); byte[] data = stream.toByteArray();
entry.setIcon(data); entry.setIcon(data, IconType.PNG);
} else { } else {
entry.setIcon(null); byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
iconBytes = IOUtils.readFile(inStream);
} catch (IOException e) {
// TODO: show dialog
throw new RuntimeException(e);
}
entry.setIcon(iconBytes, _selectedIcon.getIconType());
}
} else {
entry.setIcon(null, IconType.INVALID);
} }
} }
@ -626,7 +691,7 @@ public class EditEntryActivity extends AegisActivity {
} }
} }
private TextWatcher _iconChangeListener = new TextWatcher() { private final TextWatcher _iconChangeListener = new TextWatcher() {
@Override @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { public void beforeTextChanged(CharSequence s, int start, int count, int after) {
} }

View file

@ -4,6 +4,7 @@ import android.os.Bundle;
import android.view.MenuItem; import android.view.MenuItem;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
@ -13,7 +14,7 @@ import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
public class PreferencesActivity extends AegisActivity implements public class PreferencesActivity extends AegisActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private PreferencesFragment _fragment; private Fragment _fragment;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -58,18 +59,22 @@ public class PreferencesActivity extends AegisActivity implements
@Override @Override
protected void onRestoreInstanceState(final Bundle inState) { protected void onRestoreInstanceState(final Bundle inState) {
if (_fragment instanceof PreferencesFragment) {
// pass the stored result intent back to the fragment // pass the stored result intent back to the fragment
if (inState.containsKey("result")) { if (inState.containsKey("result")) {
_fragment.setResult(inState.getParcelable("result")); ((PreferencesFragment) _fragment).setResult(inState.getParcelable("result"));
}
} }
super.onRestoreInstanceState(inState); super.onRestoreInstanceState(inState);
} }
@Override @Override
protected void onSaveInstanceState(final Bundle outState) { protected void onSaveInstanceState(final Bundle outState) {
if (_fragment instanceof PreferencesFragment) {
// save the result intent of the fragment // save the result intent of the fragment
// this is done so we don't lose anything if the fragment calls recreate on this activity // this is done so we don't lose anything if the fragment calls recreate on this activity
outState.putParcelable("result", _fragment.getResult()); outState.putParcelable("result", ((PreferencesFragment) _fragment).getResult());
}
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
} }
@ -86,7 +91,7 @@ public class PreferencesActivity extends AegisActivity implements
@Override @Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
_fragment = (PreferencesFragment) getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment()); _fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
_fragment.setArguments(pref.getExtras()); _fragment.setArguments(pref.getExtras());
_fragment.setTargetFragment(caller, 0); _fragment.setTargetFragment(caller, 0);
showFragment(_fragment); showFragment(_fragment);
@ -95,7 +100,7 @@ public class PreferencesActivity extends AegisActivity implements
return true; return true;
} }
private void showFragment(PreferencesFragment fragment) { private void showFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right) .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.content, fragment) .replace(R.id.content, fragment)

View file

@ -0,0 +1,157 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
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.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;
import com.google.android.material.textfield.TextInputEditText;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class IconPickerDialog {
private IconPickerDialog() {
}
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
BottomSheetDialog dialog = new BottomSheetDialog(activity);
dialog.setContentView(view);
dialog.create();
FrameLayout rootView = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
rootView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
IconAdapter adapter = new IconAdapter(dialog.getContext(), issuer, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
dialog.dismiss();
listener.onIconSelected(icon);
}
@Override
public void onCustomSelected() {
dialog.dismiss();
listener.onCustomSelected();
}
});
class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
@NonNull
@Override
public List<IconPack.Icon> getPreloadItems(int position) {
IconPack.Icon icon = adapter.getIconAt(position);
return Collections.singletonList(icon);
}
@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);
}
}
TextInputEditText iconSearch = view.findViewById(R.id.text_search_icon);
iconSearch.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(rootView);
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
iconSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String query = s.toString();
adapter.setQuery(query.isEmpty() ? null : query);
}
@Override
public void afterTextChanged(Editable s) {
}
});
ViewPreloadSizeProvider<IconPack.Icon> preloadSizeProvider = new ViewPreloadSizeProvider<>();
IconPreloadProvider modelProvider = new IconPreloadProvider();
RecyclerViewPreloader<IconPack.Icon> preloader = new RecyclerViewPreloader<>(activity, modelProvider, preloadSizeProvider, 10);
IconRecyclerView recyclerView = view.findViewById(R.id.list_icons);
GridLayoutManager layoutManager = recyclerView.getGridLayoutManager();
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (adapter.getItemViewType(position) == R.layout.card_icon) {
return 1;
}
return recyclerView.getSpanCount();
}
});
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.addOnScrollListener(preloader);
adapter.loadIcons(iconPacks.get(0));
textIconPack.setText(iconPacks.get(0).getName());
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
List<String> iconPackNames = iconPacks.stream()
.map(IconPack::getName)
.collect(Collectors.toList());
PopupMenu popupMenu = new PopupMenu(activity, v);
popupMenu.setOnMenuItemClickListener(item -> {
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
adapter.loadIcons(pack);
adapter.setQuery(iconSearch.getText().toString());
textIconPack.setText(pack.getName());
return true;
});
Menu menu = popupMenu.getMenu();
for (String name : iconPackNames) {
menu.add(name);
}
popupMenu.show();
});
return dialog;
}
}

View file

@ -0,0 +1,159 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconPackException;
import com.beemdevelopment.aegis.icons.IconPackExistsException;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ImportIconPackTask;
import com.beemdevelopment.aegis.ui.views.IconPackAdapter;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class IconPacksManagerFragment extends Fragment implements IconPackAdapter.Listener {
private static final int CODE_IMPORT = 0;
private IconPackAdapter _adapter;
private IconPackManager _iconPackManager;
private View _iconPacksView;
private RecyclerView _iconPacksRecyclerView;
private LinearLayout _noIconPacksView;
private FabScrollHelper _fabScrollHelper;
public IconPacksManagerFragment() {
super(R.layout.fragment_icon_packs);
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
_iconPackManager = ((AegisApplication) getContext().getApplicationContext()).getIconPackManager();
FloatingActionButton fab = view.findViewById(R.id.fab);
fab.setOnClickListener(v -> startImportIconPack());
_fabScrollHelper = new FabScrollHelper(fab);
_noIconPacksView = view.findViewById(R.id.vEmptyList);
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
_iconPacksView = view.findViewById(R.id.view_icon_packs);
_adapter = new IconPackAdapter(this);
_iconPacksRecyclerView = view.findViewById(R.id.list_icon_packs);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
_iconPacksRecyclerView.setLayoutManager(layoutManager);
_iconPacksRecyclerView.setAdapter(_adapter);
_iconPacksRecyclerView.setNestedScrollingEnabled(false);
_iconPacksRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
_fabScrollHelper.onScroll(dx, dy);
}
});
for (IconPack pack : _iconPackManager.getIconPacks()) {
_adapter.addIconPack(pack);
}
updateEmptyState();
}
@Override
public void onRemoveIconPack(IconPack pack) {
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
.setTitle(R.string.remove_icon_pack)
.setMessage(R.string.remove_icon_pack_description)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
try {
_iconPackManager.removeIconPack(pack);
} catch (IconPackException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
return;
}
_adapter.removeIconPack(pack);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE_IMPORT && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
importIconPack(data.getData());
}
}
private void importIconPack(Uri uri) {
ImportIconPackTask task = new ImportIconPackTask(getContext(), result -> {
Exception e = result.getException();
if (e instanceof IconPackExistsException) {
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
.setTitle(R.string.error_occurred)
.setMessage(R.string.icon_pack_import_exists_error)
.setPositiveButton(R.string.yes, (dialog, which) -> {
if (removeIconPack(((IconPackExistsException) e).getIconPack())) {
importIconPack(uri);
}
})
.setNegativeButton(R.string.no, null)
.create());
} else if (e != null) {
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_import_error, e);
} else {
_adapter.addIconPack(result.getIconPack());
updateEmptyState();
}
});
task.execute(getLifecycle(), new ImportIconPackTask.Params(_iconPackManager, uri));
}
private boolean removeIconPack(IconPack pack) {
try {
_iconPackManager.removeIconPack(pack);
} catch (IconPackException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
return false;
}
_adapter.removeIconPack(pack);
updateEmptyState();
return true;
}
private void startImportIconPack() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("application/zip");
startActivityForResult(intent, CODE_IMPORT);
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_iconPacksView.setVisibility(View.VISIBLE);
_noIconPacksView.setVisibility(View.GONE);
} else {
_iconPacksView.setVisibility(View.GONE);
_noIconPacksView.setVisibility(View.VISIBLE);
}
}
}

View file

@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.ui.glide; package com.beemdevelopment.aegis.ui.glide;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -9,7 +10,9 @@ 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;
import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.AppGlideModule;
import com.caverock.androidsvg.SVG;
import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@GlideModule @GlideModule
@ -17,5 +20,8 @@ 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(VaultEntry.class, ByteBuffer.class, new IconLoader.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

@ -2,9 +2,11 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
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.Options; import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoader;
@ -14,6 +16,8 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> { public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
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 VaultEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model)); return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
@ -25,7 +29,7 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
} }
public static class Fetcher implements DataFetcher<ByteBuffer> { public static class Fetcher implements DataFetcher<ByteBuffer> {
private VaultEntry _model; private final VaultEntry _model;
private Fetcher(VaultEntry model) { private Fetcher(VaultEntry model) {
_model = model; _model = model;

View file

@ -0,0 +1,29 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.caverock.androidsvg.SVG;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class SvgBytesDecoder implements ResourceDecoder<ByteBuffer, SVG> {
private SvgDecoder _decoder = new SvgDecoder();
@Override
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
return _decoder.handles(inStream, options);
}
}
public Resource<SVG> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options) throws IOException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
return _decoder.decode(inStream, width, height, options);
}
}
}

View file

@ -0,0 +1,44 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.icons.IconType;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.IOException;
import java.io.InputStream;
import static com.bumptech.glide.request.target.Target.SIZE_ORIGINAL;
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
/** Decodes an SVG internal representation from an {@link InputStream}. */
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;
}
public Resource<SVG> decode(
@NonNull InputStream source, int width, int height, @NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
if (width != SIZE_ORIGINAL) {
svg.setDocumentWidth(width);
}
if (height != SIZE_ORIGINAL) {
svg.setDocumentHeight(height);
}
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}

View file

@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.ui.glide;
import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.caverock.androidsvg.SVG;
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(
@NonNull Resource<SVG> toTranscode, @NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}

View file

@ -35,6 +35,7 @@ public class ImportFileTask extends ProgressDialogTask<Uri, ImportFileTask.Resul
return new Result(tempFile, null); return new Result(tempFile, null);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace();
return new Result(null, e); return new Result(null, e);
} }
} }

View file

@ -0,0 +1,95 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconPackException;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.util.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ImportIconPackTask extends ProgressDialogTask<ImportIconPackTask.Params, ImportIconPackTask.Result> {
private final ImportIconPackTask.Callback _cb;
public ImportIconPackTask(Context context, ImportIconPackTask.Callback cb) {
super(context, context.getString(R.string.importing_icon_pack));
_cb = cb;
}
@Override
protected ImportIconPackTask.Result doInBackground(ImportIconPackTask.Params... params) {
Context context = getDialog().getContext();
ImportIconPackTask.Params param = params[0];
File tempFile = null;
try {
tempFile = File.createTempFile("icon-pack-", "", context.getCacheDir());
try (InputStream inStream = context.getContentResolver().openInputStream(param.getUri());
FileOutputStream outStream = new FileOutputStream(tempFile)) {
IOUtils.copy(inStream, outStream);
}
IconPack pack = param.getManager().importPack(tempFile);
return new Result(pack, null);
} catch (IOException | IconPackException e) {
e.printStackTrace();
return new ImportIconPackTask.Result(null, e);
} finally {
if (tempFile != null) {
tempFile.delete();
}
}
}
@Override
protected void onPostExecute(ImportIconPackTask.Result result) {
super.onPostExecute(result);
_cb.onTaskFinished(result);
}
public interface Callback {
void onTaskFinished(ImportIconPackTask.Result result);
}
public static class Params {
private final IconPackManager _manager;
private final Uri _uri;
public Params(IconPackManager manager, Uri uri) {
_manager = manager;
_uri = uri;
}
public IconPackManager getManager() {
return _manager;
}
public Uri getUri() {
return _uri;
}
}
public static class Result {
private final IconPack _pack;
private final Exception _e;
public Result(IconPack pack, Exception e) {
_pack = pack;
_e = e;
}
public IconPack getIconPack() {
return _pack;
}
public Exception getException() {
return _e;
}
}
}

View file

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper; 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;
@ -21,10 +22,10 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfo;
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.ui.glide.IconLoader;
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; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.zxing.common.StringUtils;
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;
@ -141,9 +142,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public void loadIcon(Fragment fragment) { public void loadIcon(Fragment fragment) {
if (_entry.hasIcon()) { if (_entry.hasIcon()) {
IconViewHelper.setLayerType(_profileDrawable, _entry.getIconType());
Glide.with(fragment) Glide.with(fragment)
.asDrawable() .asDrawable()
.load(_entry) .load(_entry)
.set(IconLoader.ICON_TYPE, _entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false) .skipMemoryCache(false)
.into(_profileDrawable); .into(_profileDrawable);

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -31,6 +32,7 @@ import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
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.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader; import com.bumptech.glide.ListPreloader;
@ -486,10 +488,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Nullable @Nullable
@Override @Override
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) { public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
return Glide.with(EntryListView.this) return Glide.with(EntryListView.this)
.asDrawable() .asDrawable()
.load(entry) .load(entry)
.set(IconLoader.ICON_TYPE, entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false); .skipMemoryCache(false);
} }

View file

@ -0,0 +1,288 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final Context _context;
private final String _issuer;
private final Listener _listener;
private IconPack _pack;
private List<IconPack.Icon> _icons;
private final List<CategoryHeader> _categories;
private String _query;
public IconAdapter(@NonNull Context context, String issuer, @NonNull Listener listener) {
_context = context;
_issuer = issuer;
_listener = listener;
_icons = new ArrayList<>();
_categories = new ArrayList<>();
}
/**
* Loads all icons from the given icon pack into this adapter. Any icons added before this call will be overwritten.
*/
public void loadIcons(IconPack pack) {
_pack = pack;
_query = null;
_icons = new ArrayList<>(_pack.getIcons());
_categories.clear();
Comparator<IconPack.Icon> iconCategoryComparator = (i1, i2) -> {
String c1 = getCategoryString(i1.getCategory());
String c2 = getCategoryString(i2.getCategory());
return c1.compareTo(c2);
};
Collections.sort(_icons, iconCategoryComparator.thenComparing(IconPack.Icon::getName));
long categoryCount = _icons.stream()
.map(IconPack.Icon::getCategory)
.filter(Objects::nonNull)
.distinct()
.count();
List<IconPack.Icon> suggested = pack.getSuggestedIcons(_issuer);
suggested.add(0, new DummyIcon(_context.getString(R.string.icon_custom)));
if (suggested.size() > 0) {
CategoryHeader category = new CategoryHeader(_context.getString(R.string.suggested));
category.setIsCollapsed(false);
category.getIcons().addAll(suggested);
_categories.add(category);
}
CategoryHeader category = null;
for (IconPack.Icon icon : _icons) {
String iconCategory = getCategoryString(icon.getCategory());
if (category == null || !getCategoryString(category.getCategory()).equals(iconCategory)) {
boolean collapsed = !(categoryCount == 0 && category == null);
category = new CategoryHeader(iconCategory);
category.setIsCollapsed(collapsed);
_categories.add(category);
}
category.getIcons().add(icon);
}
_icons.addAll(0, suggested);
updateCategoryPositions();
notifyDataSetChanged();
}
public void setQuery(@Nullable String query) {
_query = query;
if (_query == null) {
loadIcons(_pack);
} else {
_icons = _pack.getIcons().stream()
.filter(i -> i.isSuggestedFor(query))
.collect(Collectors.toList());
Collections.sort(_icons, Comparator.comparing(IconPack.Icon::getName));
notifyDataSetChanged();
}
}
public IconPack.Icon getIconAt(int position) {
if (isQueryActive()) {
return _icons.get(position);
}
position = translateIconPosition(position);
return _icons.get(position);
}
public CategoryHeader getCategoryAt(int position) {
return _categories.stream()
.filter(c -> c.getPosition() == position)
.findFirst()
.orElse(null);
}
private String getCategoryString(String category) {
return category == null ? _context.getString(R.string.uncategorized) : category;
}
private boolean isCategoryPosition(int position) {
if (isQueryActive()) {
return false;
}
return getCategoryAt(position) != null;
}
private int translateIconPosition(int position) {
int offset = 0;
for (CategoryHeader category : _categories) {
if (category.getPosition() < position) {
offset++;
if (category.isCollapsed()) {
offset -= category.getIcons().size();
}
}
}
return position - offset;
}
private void updateCategoryPositions() {
int i = 0;
for (CategoryHeader category : _categories) {
category.setPosition(i);
int icons = 0;
if (!category.isCollapsed()) {
icons = category.getIcons().size();
}
i += 1 + icons;
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
return viewType == R.layout.card_icon ? new IconHolder(view) : new IconCategoryHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (!isCategoryPosition(position)) {
IconHolder iconHolder = (IconHolder) holder;
IconPack.Icon icon = getIconAt(position);
iconHolder.setData(icon);
iconHolder.loadIcon(_context);
iconHolder.itemView.setOnClickListener(v -> {
if (icon instanceof DummyIcon) {
_listener.onCustomSelected();
} else {
_listener.onIconSelected(icon);
}
});
} else {
IconCategoryHolder categoryHolder = (IconCategoryHolder) holder;
CategoryHeader category = getCategoryAt(position);
categoryHolder.setData(category);
categoryHolder.itemView.setOnClickListener(v -> {
boolean collapsed = !category.isCollapsed();
categoryHolder.setIsCollapsed(collapsed);
category.setIsCollapsed(collapsed);
int startPosition = category.getPosition() + 1;
if (category.isCollapsed()) {
notifyItemRangeRemoved(startPosition, category.getIcons().size());
} else {
notifyItemRangeInserted(startPosition, category.getIcons().size());
}
updateCategoryPositions();
});
}
}
@Override
public int getItemCount() {
if (isQueryActive()) {
return _icons.size();
}
int items = _categories.stream()
.filter(c -> !c.isCollapsed())
.mapToInt(c -> c.getIcons().size())
.sum();
return items + _categories.size();
}
@Override
public int getItemViewType(int position) {
if (isCategoryPosition(position)) {
return R.layout.card_icon_category;
}
return R.layout.card_icon;
}
private boolean isQueryActive() {
return _query != null;
}
public interface Listener {
void onIconSelected(IconPack.Icon icon);
void onCustomSelected();
}
public static class DummyIcon extends IconPack.Icon {
private final String _name;
protected DummyIcon(String name) {
super(null, null, null);
_name = name;
}
@Override
public String getName() {
return _name;
}
@Override
public IconType getIconType() {
return null;
}
}
public static class CategoryHeader {
private final String _category;
private int _position = -1;
private final List<IconPack.Icon> _icons;
private boolean _collapsed = true;
public CategoryHeader(String category) {
_category = category;
_icons = new ArrayList<>();
}
public String getCategory() {
return _category;
}
public int getPosition() {
return _position;
}
public void setPosition(int position) {
_position = position;
}
public List<IconPack.Icon> getIcons() {
return _icons;
}
public boolean isCollapsed() {
return _collapsed;
}
public void setIsCollapsed(boolean collapsed) {
_collapsed = collapsed;
}
}
}

View file

@ -0,0 +1,36 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
public class IconCategoryHolder extends RecyclerView.ViewHolder {
private final TextView _textView;
private final ImageView _imgView;
public IconCategoryHolder(final View view) {
super(view);
_textView = view.findViewById(R.id.icon_category);
_imgView = view.findViewById(R.id.icon_category_indicator);
}
public void setData(IconAdapter.CategoryHeader header) {
_textView.setText(header.getCategory());
_imgView.setRotation(getRotation(header.isCollapsed()));
}
public void setIsCollapsed(boolean collapsed) {
_imgView.animate()
.setDuration(200)
.rotation(getRotation(collapsed))
.start();
}
private static int getRotation(boolean collapsed) {
return collapsed ? 90 : 0;
}
}

View file

@ -0,0 +1,59 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
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.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.io.File;
public class IconHolder extends RecyclerView.ViewHolder {
private File _iconFile;
private IconType _iconType;
private boolean _isCustom;
private final ImageView _imageView;
private final TextView _textView;
public IconHolder(final View view) {
super(view);
_imageView = view.findViewById(R.id.icon);
_textView = view.findViewById(R.id.icon_name);
}
public void setData(IconPack.Icon icon) {
_iconFile = icon.getFile();
_iconType = icon.getIconType();
_isCustom = icon instanceof IconAdapter.DummyIcon;
_textView.setText(icon.getName());
}
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);
} 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);
}
}
}

View file

@ -0,0 +1,66 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import java.util.ArrayList;
import java.util.List;
public class IconPackAdapter extends RecyclerView.Adapter<IconPackHolder> {
private IconPackAdapter.Listener _listener;
private List<IconPack> _iconPacks;
public IconPackAdapter(IconPackAdapter.Listener listener) {
_listener = listener;
_iconPacks = new ArrayList<>();
}
public void addIconPack(IconPack pack) {
_iconPacks.add(pack);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
public void removeIconPack(IconPack pack) {
int position = _iconPacks.indexOf(pack);
_iconPacks.remove(position);
notifyItemRemoved(position);
}
@NonNull
@Override
public IconPackHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icon_pack, parent, false);
return new IconPackHolder(view);
}
@Override
public void onBindViewHolder(@NonNull IconPackHolder holder, int position) {
holder.setData(_iconPacks.get(position));
holder.setOnDeleteClickListener(v -> {
int position12 = holder.getAdapterPosition();
_listener.onRemoveIconPack(_iconPacks.get(position12));
});
}
@Override
public int getItemCount() {
return _iconPacks.size();
}
public interface Listener {
void onRemoveIconPack(IconPack pack);
}
}

View file

@ -0,0 +1,32 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
public class IconPackHolder extends RecyclerView.ViewHolder {
private final TextView _iconPackName;
private final TextView _iconPackInfo;
private final ImageView _buttonDelete;
public IconPackHolder(final View view) {
super(view);
_iconPackName = view.findViewById(R.id.text_icon_pack_name);
_iconPackInfo = view.findViewById(R.id.text_icon_pack_info);
_buttonDelete = view.findViewById(R.id.button_delete);
}
public void setData(IconPack pack) {
_iconPackName.setText(String.format("%s (v%d)", pack.getName(), pack.getVersion()));
_iconPackInfo.setText(itemView.getResources().getQuantityString(R.plurals.icon_pack_info, pack.getIcons().size(), pack.getIcons().size()));
}
public void setOnDeleteClickListener(View.OnClickListener listener) {
_buttonDelete.setOnClickListener(listener);
}
}

View file

@ -0,0 +1,63 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
// source (slightly modified for Aegis): https://github.com/chiuki/android-recyclerview/blob/745dc88/app/src/main/java/com/sqisland/android/recyclerview/AutofitRecyclerView.java
public class IconRecyclerView extends RecyclerView {
private GridLayoutManager _manager;
private int _columnWidth = -1;
private int _spanCount;
public IconRecyclerView(@NonNull Context context) {
super(context);
init(context, null);
}
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
int[] attrsArray = {
android.R.attr.columnWidth
};
TypedArray array = context.obtainStyledAttributes(attrs, attrsArray);
_columnWidth = array.getDimensionPixelSize(0, -1);
array.recycle();
}
_manager = new GridLayoutManager(getContext(), 1);
setLayoutManager(_manager);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
if (_columnWidth > 0) {
_spanCount = Math.max(1, getMeasuredWidth() / _columnWidth);
_manager.setSpanCount(_spanCount);
}
}
public GridLayoutManager getGridLayoutManager() {
return _manager;
}
public int getSpanCount() {
return _spanCount;
}
}

View file

@ -1,7 +1,5 @@
package com.beemdevelopment.aegis.vault; package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONArray; import org.json.JSONArray;
@ -9,7 +7,7 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
public class Vault { public class Vault {
private static final int VERSION = 1; private static final int VERSION = 2;
private UUIDMap<VaultEntry> _entries = new UUIDMap<>(); private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
public JSONObject toJson() { public JSONObject toJson() {
@ -34,7 +32,7 @@ public class Vault {
try { try {
int ver = obj.getInt("version"); int ver = obj.getInt("version");
if (ver != VERSION) { if (ver > VERSION) {
throw new VaultException("Unsupported version"); throw new VaultException("Unsupported version");
} }
@ -43,7 +41,7 @@ public class Vault {
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i)); VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
entries.add(entry); entries.add(entry);
} }
} catch (EncodingException | OtpInfoException | JSONException e) { } catch (VaultEntryException | JSONException e) {
throw new VaultException(e); throw new VaultException(e);
} }

View file

@ -2,10 +2,12 @@ package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64; import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException; 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;
import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException; import org.json.JSONException;
@ -21,6 +23,7 @@ public class VaultEntry extends UUIDMap.Value {
private String _group; private String _group;
private OtpInfo _info; private OtpInfo _info;
private byte[] _icon; private byte[] _icon;
private IconType _iconType = IconType.INVALID;
private VaultEntry(UUID uuid, OtpInfo info) { private VaultEntry(UUID uuid, OtpInfo info) {
super(uuid); super(uuid);
@ -59,6 +62,7 @@ public class VaultEntry extends UUIDMap.Value {
obj.put("issuer", _issuer); obj.put("issuer", _issuer);
obj.put("group", _group); obj.put("group", _group);
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon)); obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType());
obj.put("info", _info.toJson()); obj.put("info", _info.toJson());
} catch (JSONException e) { } catch (JSONException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -67,7 +71,8 @@ public class VaultEntry extends UUIDMap.Value {
return obj; return obj;
} }
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, EncodingException { public static VaultEntry fromJson(JSONObject obj) throws VaultEntryException {
try {
// if there is no uuid, generate a new one // if there is no uuid, generate a new one
UUID uuid; UUID uuid;
if (!obj.has("uuid")) { if (!obj.has("uuid")) {
@ -84,10 +89,21 @@ public class VaultEntry extends UUIDMap.Value {
Object icon = obj.get("icon"); Object icon = obj.get("icon");
if (icon != JSONObject.NULL) { if (icon != JSONObject.NULL) {
entry.setIcon(Base64.decode((String) 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) {
throw new VaultEntryException(e);
}
} }
public String getName() { public String getName() {
@ -106,6 +122,10 @@ public class VaultEntry extends UUIDMap.Value {
return _icon; return _icon;
} }
public IconType getIconType() {
return _iconType;
}
public OtpInfo getInfo() { public OtpInfo getInfo() {
return _info; return _info;
} }
@ -126,8 +146,9 @@ public class VaultEntry extends UUIDMap.Value {
_info = info; _info = info;
} }
public void setIcon(byte[] icon) { public void setIcon(byte[] icon, IconType iconType) {
_icon = icon; _icon = icon;
_iconType = iconType;
} }
public boolean hasIcon() { public boolean hasIcon() {
@ -154,7 +175,8 @@ public class VaultEntry extends UUIDMap.Value {
&& getIssuer().equals(entry.getIssuer()) && getIssuer().equals(entry.getIssuer())
&& Objects.equals(getGroup(), entry.getGroup()) && Objects.equals(getGroup(), entry.getGroup())
&& getInfo().equals(entry.getInfo()) && getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon()); && Arrays.equals(getIcon(), entry.getIcon())
&& getIconType().equals(entry.getIconType());
} }
/** /**

View file

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

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
</vector>

View file

@ -0,0 +1,8 @@
<!-- drawable/dots_vertical.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" />
</vector>

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" />
</vector>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="5dp"
android:layout_width="75dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/icon"
android:layout_height="75dp"
android:layout_width="match_parent"/>
<TextView
android:id="@+id/icon_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center" />
</LinearLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingVertical="5dp"
android:paddingStart="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/icon_category"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="15sp" />
<ImageView
android:id="@+id/icon_category_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_chevron_down_black_24dp"
app:tint="?attr/iconColorPrimary"
android:layout_gravity="center_vertical" />
</LinearLayout>

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/button_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:paddingTop="12.5dp"
android:paddingBottom="12.5dp"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text_icon_pack_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/text_icon_pack_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:layout_marginTop="12.5dp"
android:layout_marginBottom="12.5dp"/>
<ImageView
android:id="@+id/button_delete"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:paddingStart="15dp"
android:paddingTop="12.5dp"
android:paddingEnd="15dp"
android:paddingBottom="12.5dp"
android:src="@drawable/ic_delete_black_24dp"
app:tint="?attr/iconColorPrimary" />
</LinearLayout>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="10dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="16dp"
android:layout_height="2dp"
android:layout_gravity="center"
android:background="@drawable/drag_handle" />
<TextView
android:paddingTop="8dp"
android:textSize="17sp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/primaryText"
android:text="@string/pick_icon" />
<TextView
android:id="@+id/text_icon_pack"
android:paddingTop="2dp"
android:paddingBottom="16dp"
android:textSize="13sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
<ImageButton
android:id="@+id/btn_icon_pack"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="5dp"
android:layout_marginTop="12dp"
android:scaleType="centerCrop"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_dots_vertical_black_24dp"
android:tint="?attr/iconColorPrimary"/>
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider2" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="15dp"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:hint="@string/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:endIconMode="clear_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_search_icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<com.beemdevelopment.aegis.ui.views.IconRecyclerView
android:id="@+id/list_icons"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="vertical"
android:layout_marginTop="10dp"
android:columnWidth="75dp" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/cardBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|fill_vertical"
android:id="@+id/vEmptyList"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="150dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/ic_package_variant_black_24dp"
app:tint="?attr/primaryText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_icon_packs_title"
android:paddingTop="17dp"
android:textColor="?attr/primaryText"
android:textSize="18sp" />
<TextView
android:id="@+id/txt_no_icon_packs"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:lineSpacingExtra="5dp"
android:paddingTop="7dp"
android:text="@string/no_icon_packs"
android:textAlignment="center" />
</LinearLayout>
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/view_icon_packs"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_icon_packs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_add_black_24dp"
app:tint="@color/icon_primary_dark">
</com.google.android.material.floatingactionbutton.FloatingActionButton>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -13,6 +13,7 @@
<string name="discard">Discard</string> <string name="discard">Discard</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="issuer">Issuer</string> <string name="issuer">Issuer</string>
<string name="suggested">Suggested</string>
<string name="settings">Preferences</string> <string name="settings">Preferences</string>
<string name="pref_cat_appearance_app">App</string> <string name="pref_cat_appearance_app">App</string>
@ -29,6 +30,8 @@
<string name="pref_section_import_export_summary">Import backups of Aegis or other authenticator apps. Create manual exports of your Aegis vault.</string> <string name="pref_section_import_export_summary">Import backups of Aegis or other authenticator apps. Create manual exports of your Aegis vault.</string>
<string name="pref_section_backups_title">Backups</string> <string name="pref_section_backups_title">Backups</string>
<string name="pref_section_backups_summary">Set up automatic backups to a location of your choosing or enable participation in Android\'s backup system.</string> <string name="pref_section_backups_summary">Set up automatic backups to a location of your choosing or enable participation in Android\'s backup system.</string>
<string name="pref_section_icon_packs">Icon packs</string>
<string name="pref_section_icon_packs_summary">Manage and import icon packs</string>
<string name="pref_select_theme_title">Theme</string> <string name="pref_select_theme_title">Theme</string>
<string name="pref_view_mode_title">View mode</string> <string name="pref_view_mode_title">View mode</string>
<string name="pref_lang_title">Language</string> <string name="pref_lang_title">Language</string>
@ -149,6 +152,7 @@
<string name="encrypting_vault">Encrypting the vault</string> <string name="encrypting_vault">Encrypting the vault</string>
<string name="exporting_vault">Exporting the vault</string> <string name="exporting_vault">Exporting the vault</string>
<string name="reading_file">Reading file</string> <string name="reading_file">Reading file</string>
<string name="importing_icon_pack">Importing icon pack</string>
<string name="delete_entry">Delete entry</string> <string name="delete_entry">Delete entry</string>
<string name="delete_entry_description">Are you sure you want to delete this entry?</string> <string name="delete_entry_description">Are you sure you want to delete this entry?</string>
<string name="delete_entry_explanation">This action does not disable 2FA for <b>%s</b>. To prevent losing access, make sure that you have disabled 2FA or that you have an alternative way to generate codes for this service.</string> <string name="delete_entry_explanation">This action does not disable 2FA for <b>%s</b>. To prevent losing access, make sure that you have disabled 2FA or that you have an alternative way to generate codes for this service.</string>
@ -185,6 +189,15 @@
<string name="backup_successful">The backup was scheduled successfully</string> <string name="backup_successful">The backup was scheduled successfully</string>
<string name="backup_error">An error occurred while trying to create a backup</string> <string name="backup_error">An error occurred while trying to create a backup</string>
<string name="documentsui_error">DocumentsUI appears to be missing from your device. This is an important system component necessary for the selection and creation of documents. If you used a tool to &quot;debloat&quot; your device, you may have accidentally deleted it and will have to reinstall it.</string> <string name="documentsui_error">DocumentsUI appears to be missing from your device. This is an important system component necessary for the selection and creation of documents. If you used a tool to &quot;debloat&quot; your device, you may have accidentally deleted it and will have to reinstall it.</string>
<string name="icon_pack_import_error">An error occurred while trying to import an icon pack</string>
<string name="icon_pack_import_exists_error">The icon pack you\'re trying to import already exists. Do you want to overwrite it?</string>
<string name="icon_pack_delete_error">An error occurred while trying to delete an icon pack</string>
<plurals name="icon_pack_info">
<item quantity="one">%d icon</item>
<item quantity="other">%d icons</item>
</plurals>
<string name="icon_pack">Icon pack</string>
<string name="icon_custom">Custom</string>
<string name="permission_denied">Permission denied</string> <string name="permission_denied">Permission denied</string>
<string name="andotp_new_format">New format (v0.6.3 or newer) </string> <string name="andotp_new_format">New format (v0.6.3 or newer) </string>
<string name="andotp_old_format">Old format (v0.6.2 or older) </string> <string name="andotp_old_format">Old format (v0.6.2 or older) </string>
@ -219,6 +232,8 @@
<string name="remove_slot_description">Are you sure you want to remove this slot?</string> <string name="remove_slot_description">Are you sure you want to remove this slot?</string>
<string name="remove_group">Remove group</string> <string name="remove_group">Remove group</string>
<string name="remove_group_description">Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'.</string> <string name="remove_group_description">Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'.</string>
<string name="remove_icon_pack">Remove icon pack</string>
<string name="remove_icon_pack_description">Are you sure you want to remove this icon pack? Entries that use icons from this pack will not be affected.</string>
<string name="adding_new_slot_error">An error occurred while trying to add a new slot:</string> <string name="adding_new_slot_error">An error occurred while trying to add a new slot:</string>
<string name="progressbar_error">Unable to reset animator duration scale. Progress bars will be invisible.</string> <string name="progressbar_error">Unable to reset animator duration scale. Progress bars will be invisible.</string>
<string name="details">Details</string> <string name="details">Details</string>
@ -237,6 +252,7 @@
<string name="group_name_hint">Group name</string> <string name="group_name_hint">Group name</string>
<string name="preference_manage_groups">Edit groups</string> <string name="preference_manage_groups">Edit groups</string>
<string name="preference_manage_groups_summary">Manage and delete your groups here</string> <string name="preference_manage_groups_summary">Manage and delete your groups here</string>
<string name="pref_highlight_entry_title">Highlight tokens when tapped</string> <string name="pref_highlight_entry_title">Highlight tokens when tapped</string>
<string name="pref_highlight_entry_summary">Make tokens easier to distinguish from each other by temporarily highlighting them when tapped</string> <string name="pref_highlight_entry_summary">Make tokens easier to distinguish from each other by temporarily highlighting them when tapped</string>
<string name="pref_copy_on_tap_title">Copy tokens when tapped</string> <string name="pref_copy_on_tap_title">Copy tokens when tapped</string>
@ -320,6 +336,10 @@
<string name="empty_list_title">No entries found</string> <string name="empty_list_title">No entries found</string>
<string name="empty_group_list">There are no groups to be shown. Add groups in the edit screen of an entry</string> <string name="empty_group_list">There are no groups to be shown. Add groups in the edit screen of an entry</string>
<string name="empty_group_list_title">No groups found</string> <string name="empty_group_list_title">No groups found</string>
<string name="no_icon_packs">No icon packs have been imported yet. Tap the plus sign to import one. Tip: try <a href="https://github.com/aegis-icons/aegis-icons/releases/latest">krisu5\'s icon pack</a>.</string>
<string name="no_icon_packs_title">No icon packs</string>
<string name="pick_icon">Pick an icon</string>
<string name="uncategorized">Uncategorized</string>
<string name="done">Done</string> <string name="done">Done</string>
<plurals name="entries_count"> <plurals name="entries_count">
<item quantity="one">%d / %d entry</item> <item quantity="one">%d / %d entry</item>

View file

@ -202,6 +202,7 @@
</style> </style>
<style name="ThemeOverLay.Aegis.BottomSheetDialog.Rounded" parent="@style/ThemeOverlay.MaterialComponents.BottomSheetDialog"> <style name="ThemeOverLay.Aegis.BottomSheetDialog.Rounded" parent="@style/ThemeOverlay.MaterialComponents.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
<item name="bottomSheetStyle">@style/Widget.Aegis.BottomSheet.Rounded</item> <item name="bottomSheetStyle">@style/Widget.Aegis.BottomSheet.Rounded</item>
</style> </style>

View file

@ -16,6 +16,12 @@
app:title="@string/pref_section_behavior_title" app:title="@string/pref_section_behavior_title"
app:summary="@string/pref_section_behavior_summary" /> app:summary="@string/pref_section_behavior_summary" />
<Preference
android:fragment="com.beemdevelopment.aegis.ui.fragments.IconPacksManagerFragment"
android:title="@string/pref_section_icon_packs"
android:summary="@string/pref_section_icon_packs_summary"
app:icon="@drawable/ic_package_variant_black_24dp"/>
<Preference <Preference
android:fragment="com.beemdevelopment.aegis.ui.fragments.SecurityPreferencesFragment" android:fragment="com.beemdevelopment.aegis.ui.fragments.SecurityPreferencesFragment"
app:icon="@drawable/ic_vpn_key_black_24dp" app:icon="@drawable/ic_vpn_key_black_24dp"

62
docs/iconpacks.md Normal file
View file

@ -0,0 +1,62 @@
# Icon packs
### The format
Icon packs are .ZIP archives with a collection of icons and a ``pack.json``
file. The icon pack definition is a JSON file, formatted like the example below.
All icon packs have a name, a UUID, a version and a list of icons. The version
number is incremented when a new version of the icon pack is released. The UUID
is randomly generated once and stays the same across different versions.
```json
{
"uuid": "c553f06f-2a17-46ca-87f5-56af90dd0500",
"name": "Alex' Icon Pack",
"version": 1,
"icons": [
{
"filename": "services/Google.png",
"category": "Services",
"issuer": [ "google" ]
},
{
"filename": "services/Blizzard.png",
"category": "Gaming",
"issuer": [ "blizzard", "battle.net" ]
}
]
}
```
Every icon definition contains the filename of the icon file, relative to the
root of the .ZIP archive. Icon definitions also have a list of strings that the
Issuer field in Aegis is matched against for automatic selection of an icon for
new entries. Matching is done in a case-insensitve manner. There's also a
category field.
The following image formats are supported, in order of preference:
| Name | MIME | Extension |
|:-----|:--------------|:----------|
| SVG | image/svg+xml | .svg |
| PNG | image/png | .png |
| JPEG | image/jpeg | .jpg |
Any files in the .ZIP archive that are not the ``pack.json`` file or referred to
in the icons list are ignored. Such files are not extracted when importing the
icon pack into Aegis.
### Using icon packs in Aegis
Users can download an icon pack from the internet and import it into Aegis
through the settings menu. Aegis extracts the icon pack to
``icons/{uuid}/{version}``, relative to its internal storage directory. So for
the example icon pack above, that'd be:
``icons/c553f06f-2a17-46ca-87f5-56af90dd0500/1``. If it has an old version of
the icon pack, it will be removed after successful extraction of the newer
version.
After that, Aegis will start proposing icons for new entries if the issuer
matches with one of the icons in the pack. We'll also have an icon selection
dialog, where all of the icons in the pack appear. When the user selects an
icon, it is copied and stored in the vault file.