Merge pull request #1172 from michaelschattgen/feature/assign-icons

Add ability to automatically assign icons to (imported) entries
This commit is contained in:
Alexander Bakker 2023-09-11 21:07:23 +02:00 committed by GitHub
commit 9414b5c420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 717 additions and 15 deletions

View file

@ -82,6 +82,8 @@
<activity
android:name=".ui.GroupManagerActivity"
android:label="@string/title_activity_manage_groups" />
<activity android:name=".AssignIconsActivity"
android:label="@string/title_activity_assign_icons"/>
<activity
android:name=".ui.PanicResponderActivity"
android:exported="true"

View file

@ -0,0 +1,278 @@
package com.beemdevelopment.aegis;
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.AegisActivity;
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.models.AssignIconEntry;
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class AssignIconsActivity extends AegisActivity implements AssignIconAdapter.Listener {
private AssignIconAdapter _adapter;
private ArrayList<AssignIconEntry> _entries = new ArrayList<>();
private RecyclerView _entriesView;
private AssignIconsActivity.BackPressHandler _backPressHandler;
private ViewPreloadSizeProvider<AssignIconEntry> _preloadSizeProvider;
private IconPack _favoriteIconPack;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_assign_icons);
setSupportActionBar(findViewById(R.id.toolbar));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
ArrayList<UUID> assignIconEntriesIds = (ArrayList<UUID>) getIntent().getSerializableExtra("entries");
for (UUID entryId: assignIconEntriesIds) {
VaultEntry vaultEntry = _vaultManager.getVault().getEntryByUUID(entryId);
_entries.add(new AssignIconEntry(vaultEntry));
}
_backPressHandler = new AssignIconsActivity.BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
IconPreloadProvider modelProvider1 = new IconPreloadProvider();
EntryIconPreloadProvider modelProvider2 = new EntryIconPreloadProvider();
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
RecyclerViewPreloader<IconPack.Icon> preloader1 = new RecyclerViewPreloader(this, modelProvider1, _preloadSizeProvider, 10);
RecyclerViewPreloader<VaultEntry> preloader2 = new RecyclerViewPreloader(this, modelProvider2, _preloadSizeProvider, 10);
_adapter = new AssignIconAdapter(this);
_entriesView = findViewById(R.id.list_assign_icons);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_entriesView.setLayoutManager(layoutManager);
_entriesView.setAdapter(_adapter);
_entriesView.setNestedScrollingEnabled(false);
_entriesView.addItemDecoration(new SpacesItemDecoration(8));
_entriesView.addOnScrollListener(preloader1);
_entriesView.addOnScrollListener(preloader2);
Optional<IconPack> favoriteIconPack = _iconPackManager.getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.findFirst();
if (!favoriteIconPack.isPresent()) {
throw new RuntimeException(String.format("Started %s without any icon packs present", AssignIconsActivity.class.getName()));
}
_favoriteIconPack = favoriteIconPack.get();
for (AssignIconEntry entry : _entries) {
IconPack.Icon suggestedIcon = findSuggestedIcon(entry);
if (suggestedIcon != null) {
entry.setNewIcon(suggestedIcon);
}
}
_adapter.addEntries(_entries);
}
private IconPack.Icon findSuggestedIcon(AssignIconEntry entry) {
List<IconPack.Icon> suggestedIcons = _favoriteIconPack.getSuggestedIcons(entry.getEntry().getIssuer());
if (suggestedIcons.size() > 0) {
return suggestedIcons.get(0);
}
return null;
}
private void saveAndFinish() throws IOException {
ArrayList<UUID> uuids = new ArrayList<>();
for (AssignIconEntry selectedEntry : _entries) {
VaultEntry entry = selectedEntry.getEntry();
if(selectedEntry.getNewIcon() != null) {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(selectedEntry.getNewIcon().getFile())){
iconBytes = IOUtils.readFile(inStream);
}
entry.setIcon(iconBytes, selectedEntry.getNewIcon().getIconType());
uuids.add(entry.getUUID());
_vaultManager.getVault().replaceEntry(entry);
}
}
Intent intent = new Intent();
intent.putExtra("entryUUIDs", uuids);
if (saveAndBackupVault()) {
setResult(RESULT_OK, intent);
finish();
}
}
private void discardAndFinish() {
Dialogs.showDiscardDialog(this,
(dialog, which) -> {
try {
saveAndFinish();
} catch (IOException e) {
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
}
},
(dialog, which) -> finish());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_groups, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
discardAndFinish();
break;
case R.id.action_save:
try {
saveAndFinish();
} catch (IOException e) {
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
}
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public void onAssignIconEntryClick(AssignIconEntry entry) {
BottomSheetDialog dialog = IconPickerDialog.create(this, Collections.singletonList(_favoriteIconPack), entry.getEntry().getIssuer(), false, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
entry.setNewIcon(icon);
}
@Override
public void onCustomSelected() { }
});
Dialogs.showSecureDialog(dialog);
}
@Override
public void onSetPreloadView(View view) {
_preloadSizeProvider.setView(view);
}
private class BackPressHandler extends OnBackPressedCallback {
public BackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
discardAndFinish();
}
}
private class EntryIconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
@NonNull
@Override
public List<VaultEntry> getPreloadItems(int position) {
VaultEntry entry = _entries.get(position).getEntry();
if (entry.hasIcon()) {
return Collections.singletonList(entry);
}
return Collections.emptyList();
}
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
return Glide.with(AssignIconsActivity.this)
.asDrawable()
.load(entry)
.set(IconLoader.ICON_TYPE, entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
@NonNull
@Override
public List<IconPack.Icon> getPreloadItems(int position) {
AssignIconEntry entry = _entries.get(position);
if (entry.getNewIcon() != null) {
return Collections.singletonList(entry.getNewIcon());
}
return Collections.emptyList();
}
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
return Glide.with(AssignIconsActivity.this)
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
}
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private final int _space;
public SpacesItemDecoration(int dpSpace) {
this._space = MetricsHelper.convertDpToPixels(AssignIconsActivity.this, dpSpace);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.left = _space;
outRect.right = _space;
outRect.bottom = _space;
if (parent.getChildLayoutPosition(view) == 0) {
outRect.top = _space;
}
}
}
}

View file

@ -42,6 +42,10 @@ public class IconPackManager {
return packs.get(0);
}
public boolean hasIconPack() {
return _iconPacks.size() > 0;
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}

View file

@ -216,7 +216,7 @@ public class EditEntryActivity extends AegisActivity {
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
Glide.with(this)
.asDrawable()
.load(_origEntry)
.load(_origEntry.getIcon())
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
@ -500,7 +500,7 @@ public class EditEntryActivity extends AegisActivity {
return;
}
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), true, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
selectIcon(icon);

View file

@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.Menu;
@ -14,12 +15,14 @@ import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.AssignIconsActivity;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
@ -244,8 +247,30 @@ public class ImportEntriesActivity extends AegisActivity {
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
setResult(RESULT_OK, null);
finish();
if (_iconPackManager.hasIconPack()) {
ArrayList<UUID> assignIconEntriesIds = new ArrayList<>();
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
for (ImportEntry entry : selectedEntries) {
assignIconEntriesIds.add(entry.getEntry().getUUID());
}
assignIconIntent.putExtra("entries", assignIconEntriesIds);
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_assign_icons_dialog_title)
.setMessage(R.string.import_assign_icons_dialog_text)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
startActivity(assignIconIntent);
finish();
})
.setNegativeButton(android.R.string.no, ((dialogInterface, i) -> finish()))
.create());
} else {
finish();
}
}
}

View file

@ -35,6 +35,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import com.beemdevelopment.aegis.AssignIconsActivity;
import com.beemdevelopment.aegis.CopyBehavior;
import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.Preferences;
@ -76,6 +77,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private static final int CODE_DECRYPT = 4;
private static final int CODE_PREFERENCES = 5;
private static final int CODE_SCAN_IMAGE = 6;
private static final int CODE_ASSIGN_ICONS = 7;
// Permission request codes
private static final int CODE_PERM_CAMERA = 0;
@ -232,6 +235,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
break;
case CODE_SCAN_IMAGE:
onScanImageResult(data);
break;
case CODE_ASSIGN_ICONS:
onAssignEntriesResult(data);
break;
}
super.onActivityResult(requestCode, resultCode, data);
@ -317,6 +324,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(intent, requestCode);
}
private void startAssignIconsActivity(int requestCode, List<VaultEntry> entries) {
ArrayList<UUID> assignIconEntriesIds = new ArrayList<>();
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
for (VaultEntry entry : entries) {
assignIconEntriesIds.add(entry.getUUID());
}
assignIconIntent.putExtra("entries", assignIconEntriesIds);
startActivityForResult(assignIconIntent, requestCode);
}
private void startIntroActivity() {
if (!_isDoingIntro) {
Intent intro = new Intent(this, IntroActivity.class);
@ -353,6 +371,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void onAssignEntriesResult(Intent data) {
if (_loaded) {
ArrayList<UUID> entryUUIDs = (ArrayList<UUID>) data.getSerializableExtra("entryUUIDs");
for (UUID entryUUID: entryUUIDs) {
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
}
}
private void onScanImageResult(Intent intent) {
if (intent.getData() != null) {
startDecodeQrCodeImages(Collections.singletonList(intent.getData()));
@ -916,6 +945,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected);
}
private void setAssignIconsMenuItemVisibility() {
MenuItem assignIconsMenuItem = _actionMode.getMenu().findItem(R.id.action_assign_icons);
assignIconsMenuItem.setVisible(_iconPackManager.hasIconPack());
}
private void setFavoriteMenuItemVisiblity() {
MenuItem toggleFavoriteMenuItem = _actionMode.getMenu().findItem(R.id.action_toggle_favorite);
@ -948,6 +982,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_actionMode = startSupportActionMode(_actionModeCallbacks);
_actionModeBackPressHandler.setEnabled(true);
setFavoriteMenuItemVisiblity();
setAssignIconsMenuItemVisibility();
}
@Override
@ -1142,6 +1177,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
mode.finish();
});
return true;
case R.id.action_assign_icons:
startAssignIconsActivity(CODE_ASSIGN_ICONS, _selectedEntries);
mode.finish();
return true;
default:
return false;
}

View file

@ -40,7 +40,7 @@ public class IconPickerDialog {
}
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, boolean showAddCustom, IconAdapter.Listener listener) {
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
@ -128,7 +128,7 @@ public class IconPickerDialog {
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.addOnScrollListener(preloader);
adapter.loadIcons(iconPacks.get(0));
adapter.loadIcons(iconPacks.get(0), showAddCustom);
textIconPack.setText(iconPacks.get(0).getName());
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
@ -139,7 +139,7 @@ public class IconPickerDialog {
PopupMenu popupMenu = new PopupMenu(activity, v);
popupMenu.setOnMenuItemClickListener(item -> {
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
adapter.loadIcons(pack);
adapter.loadIcons(pack, showAddCustom);
String query = iconSearch.getText().toString();
if (!query.isEmpty()) {

View file

@ -0,0 +1,40 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.io.Serializable;
public class AssignIconEntry implements Serializable {
private final VaultEntry _entry;
private IconPack.Icon _newIcon;
private transient AssignIconEntry.Listener _listener;
public void setOnResetListener(AssignIconEntry.Listener listener) {
_listener = listener;
}
public AssignIconEntry(VaultEntry entry) {
_entry = entry;
}
public VaultEntry getEntry() {
return _entry;
}
public IconPack.Icon getNewIcon() { return _newIcon; }
public void setNewIcon(IconPack.Icon icon) {
_newIcon = icon;
if (_listener != null) {
_listener.onNewIconChanged();
}
}
public interface Listener {
void onNewIconChanged();
}
}

View file

@ -0,0 +1,55 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import java.util.ArrayList;
import java.util.Collection;
public class AssignIconAdapter extends RecyclerView.Adapter<AssignIconHolder> {
private AssignIconAdapter.Listener _listener;
private ArrayList<AssignIconEntry> _entries;
public AssignIconAdapter(AssignIconAdapter.Listener listener) {
_listener = listener;
_entries = new ArrayList<>();
}
public void addEntries(Collection<AssignIconEntry> entries) {
_entries.addAll(entries);
}
@Override
public AssignIconHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_assign_icon_entry, parent, false);
AssignIconHolder holder = new AssignIconHolder(view);
// NOTE: This assumes that the old and new icon views are the same size
_listener.onSetPreloadView(holder.getOldIconView());
return holder;
}
@Override
public void onBindViewHolder(AssignIconHolder holder, int position) {
holder.setData(_entries.get(position));
holder.itemView.setOnClickListener(view -> {
_listener.onAssignIconEntryClick(_entries.get(position));
});
_entries.get(position).setOnResetListener(holder);
}
@Override
public int getItemCount() {
return _entries.size();
}
public interface Listener {
void onAssignIconEntryClick(AssignIconEntry entry);
void onSetPreloadView(View view);
}
}

View file

@ -0,0 +1,91 @@
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.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class AssignIconHolder extends RecyclerView.ViewHolder implements AssignIconEntry.Listener {
private View _view;
private AssignIconEntry _entry;
private TextView _issuer;
private TextView _accountName;
private ImageView _oldIcon;
private ImageView _newIcon;
private ImageView _btnReset;
public AssignIconHolder(final View view) {
super(view);
_view = view.findViewById(R.id.rlCardEntry);
_issuer = view.findViewById(R.id.tvIssuer);
_accountName = view.findViewById(R.id.tvAccountName);
_oldIcon = view.findViewById(R.id.ivOldImage);
_newIcon = view.findViewById(R.id.ivNewImage);
_btnReset = view.findViewById(R.id.btnReset);
_btnReset.setOnClickListener(l -> _entry.setNewIcon(null));
}
public void setData(AssignIconEntry entry) {
_entry = entry;
_issuer.setText(entry.getEntry().getIssuer());
_accountName.setText(entry.getEntry().getName());
if(!entry.getEntry().hasIcon()) {
TextDrawable drawable = TextDrawableHelper.generate(entry.getEntry().getIssuer(), entry.getEntry().getName(), _oldIcon);
_oldIcon.setImageDrawable(drawable);
} else {
Glide.with(_view.getContext())
.asDrawable()
.load(entry.getEntry())
.set(IconLoader.ICON_TYPE, entry.getEntry().getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(_oldIcon);
}
setNewIcon();
}
private void setNewIcon() {
if (_entry.getNewIcon() != null) {
Glide.with(_view.getContext())
.asDrawable()
.load(_entry.getNewIcon().getFile())
.set(IconLoader.ICON_TYPE, _entry.getNewIcon().getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_newIcon);
} else {
Glide.with(_view.getContext())
.asDrawable()
.load(R.drawable.ic_icon_unselected)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_newIcon);
}
_btnReset.setVisibility(_entry.getNewIcon() != null ? View.VISIBLE : View.INVISIBLE);
}
public View getOldIconView() {
return _oldIcon;
}
@Override
public void onNewIconChanged() {
setNewIcon();
}
}

View file

@ -200,7 +200,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
IconViewHelper.setLayerType(_profileDrawable, _entry.getIconType());
Glide.with(fragment)
.asDrawable()
.load(_entry)
.load(_entry.getIcon())
.set(IconLoader.ICON_TYPE, _entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)

View file

@ -40,7 +40,7 @@ public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
/**
* 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) {
public void loadIcons(IconPack pack, boolean showAddCustom) {
_pack = pack;
_query = null;
_icons = new ArrayList<>(_pack.getIcons());
@ -60,7 +60,11 @@ public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
.count();
List<IconPack.Icon> suggested = pack.getSuggestedIcons(_issuer);
suggested.add(0, new DummyIcon(_context.getString(R.string.icon_custom)));
if (showAddCustom) {
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);
@ -90,7 +94,7 @@ public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
_query = query;
if (_query == null) {
loadIcons(_pack);
loadIcons(_pack, false);
} else {
_icons = _pack.getIcons().stream()
.filter(i -> i.isSuggestedFor(query))

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10,17l5,-5 -5,-5v10z"/>
</vector>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M120,360L120,120L200,120L200,254Q250,192 322.5,156Q395,120 480,120Q598,120 690.5,187Q783,254 820,360L733,360Q699,288 632,244Q565,200 480,200Q423,200 372.5,221Q322,242 284,280L360,280L360,360L120,360ZM240,720L720,720L570,520L450,680L360,560L240,720ZM200,880Q167,880 143.5,856.5Q120,833 120,800L120,480L200,480L200,800Q200,800 200,800Q200,800 200,800L760,800Q760,800 760,800Q760,800 760,800L760,480L840,480L840,800Q840,833 816.5,856.5Q793,880 760,880L200,880Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/cardBackground"/>
<corners android:radius="4dp"/>
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp"/>
</shape>

View file

@ -0,0 +1,27 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.AssignIconsActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Aegis.AppBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorAppBar" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_assign_icons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:background="@drawable/rounded_background"
android:elevation="4dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:background="?attr/cardBackground"
android:id="@+id/rlCardEntry"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="14dp">
<!-- Title -->
<TextView
android:id="@+id/tvIssuer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="?attr/primaryText"
android:text="Issuer"
android:textStyle="bold"
android:textSize="18sp"/>
<TextView
android:id="@+id/tvAccountName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/secondary_text"
android:text="AccountName"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
<!-- Row with 3 columns -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivOldImage"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center_vertical" />
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
app:tint="?attr/iconColorPrimary"
android:alpha="0.7"
android:src="@drawable/baseline_arrow_right_24"
android:layout_marginStart="5dp"
android:layout_marginEnd="15dp"
android:layout_gravity="center_vertical"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivNewImage"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center_vertical" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/btnReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_reset_image"
app:tint="?attr/iconColorPrimary"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:paddingTop="12.5dp"
android:paddingBottom="12.5dp"
android:foreground="?android:attr/selectableItemBackground"
android:layout_gravity="center_vertical"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -22,18 +22,25 @@
app:showAsAction="always" />
<item
android:id="@+id/action_delete"
android:title="@string/action_delete"
android:id="@+id/action_assign_icons"
android:title="@string/assign_icons"
android:orderInCategory="100"
android:icon="@drawable/ic_delete_white"
android:tint="?attr/iconColorPrimary"
app:showAsAction="ifRoom"/>
app:showAsAction="never"/>
<item
android:id="@+id/action_share_qr"
android:title="@string/action_transfer"
android:orderInCategory="100"
android:orderInCategory="110"
android:icon="@drawable/ic_qr_code_full"
android:tint="?attr/iconColorPrimary"
app:showAsAction="always"/>
<item
android:id="@+id/action_delete"
android:title="@string/action_delete"
android:orderInCategory="400"
android:icon="@drawable/ic_delete_white"
android:tint="?attr/iconColorPrimary"
app:showAsAction="ifRoom"/>
</menu>

View file

@ -171,6 +171,7 @@
<string name="set_up_biometric">Set up biometric unlock</string>
<string name="copy">Copy</string>
<string name="edit">Edit</string>
<string name="assign_icons">Assign icons</string>
<string name="favorite" comment="Verb">Favorite</string>
<string name="unfavorite" comment="Verb">Unfavorite</string>
<string name="error_all_caps">ERROR</string>
@ -205,6 +206,7 @@
<string name="discard_changes">Discard changes?</string>
<string name="discard_changes_description">Your changes have not been saved</string>
<string name="saving_profile_error">Error saving profile</string>
<string name="saving_assign_icons_error">Error assigning icons</string>
<string name="welcome">Welcome</string>
<string name="app_description">Aegis is a free, secure and open source 2FA app</string>
<string name="setup_completed">Setup completed</string>
@ -458,6 +460,7 @@
<string name="title_activity_edit_entry">Edit entry</string>
<string name="title_activity_scan_qr">Scan a QR code</string>
<string name="title_activity_import_entries">Import entries</string>
<string name="title_activity_assign_icons">Assign icons</string>
<string name="dialog_wipe_entries_title">Wipe entries</string>
<string name="dialog_wipe_entries_message">Your vault already contains entries. Do you want to remove these entries before importing this file?\n\n<b>In doing so, you will permanently lose access to the existing entries in the vault.</b></string>
<string name="dialog_wipe_entries_checkbox">Wipe vault contents</string>
@ -489,6 +492,8 @@
<string name="importer_help_steam"><b>Steam v3.0 and newer are not supported</b>. Supply a copy of <b>/data/data/com.valvesoftware.android.steam.community/files/Steamguard-*.json</b>, located in the internal storage directory of Steam.</string>
<string name="importer_help_totp_authenticator">Supply a TOTP Authenticator export file.</string>
<string name="importer_help_winauth">Supply a WinAuth export file.</string>
<string name="import_assign_icons_dialog_title">Assign icons</string>
<string name="import_assign_icons_dialog_text">Do you want to assign icons to the imported entries?</string>
<string name="importer_encrypted_exception_google_authenticator">Encrypted entry was skipped: %s</string>