mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-21 22:39:12 +00:00
Introduce support for icon packs
This commit is contained in:
parent
c977b9a064
commit
4f38988c0d
44 changed files with 2128 additions and 54 deletions
|
@ -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}"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
183
app/src/main/java/com/beemdevelopment/aegis/icons/IconPack.java
Normal file
183
app/src/main/java/com/beemdevelopment/aegis/icons/IconPack.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
7
app/src/main/res/drawable/ic_chevron_down_black_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_chevron_down_black_24dp.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
19
app/src/main/res/layout/card_icon.xml
Normal file
19
app/src/main/res/layout/card_icon.xml
Normal 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>
|
25
app/src/main/res/layout/card_icon_category.xml
Normal file
25
app/src/main/res/layout/card_icon_category.xml
Normal 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>
|
69
app/src/main/res/layout/card_icon_pack.xml
Normal file
69
app/src/main/res/layout/card_icon_pack.xml
Normal 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>
|
82
app/src/main/res/layout/dialog_icon_picker.xml
Normal file
82
app/src/main/res/layout/dialog_icon_picker.xml
Normal 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>
|
81
app/src/main/res/layout/fragment_icon_packs.xml
Normal file
81
app/src/main/res/layout/fragment_icon_packs.xml
Normal 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>
|
|
@ -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 "debloat" 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 "debloat" 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
62
docs/iconpacks.md
Normal 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.
|
Loading…
Add table
Reference in a new issue