mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 22:12:55 +00:00
Merge pull request #1172 from michaelschattgen/feature/assign-icons
Add ability to automatically assign icons to (imported) entries
This commit is contained in:
commit
9414b5c420
20 changed files with 717 additions and 15 deletions
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
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))
|
||||
|
|
5
app/src/main/res/drawable/baseline_arrow_right_24.xml
Normal file
5
app/src/main/res/drawable/baseline_arrow_right_24.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_icon_unselected.xml
Normal file
5
app/src/main/res/drawable/ic_icon_unselected.xml
Normal file
File diff suppressed because one or more lines are too long
9
app/src/main/res/drawable/ic_reset_image.xml
Normal file
9
app/src/main/res/drawable/ic_reset_image.xml
Normal 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>
|
6
app/src/main/res/drawable/rounded_background.xml
Normal file
6
app/src/main/res/drawable/rounded_background.xml
Normal 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>
|
27
app/src/main/res/layout/activity_assign_icons.xml
Normal file
27
app/src/main/res/layout/activity_assign_icons.xml
Normal 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>
|
98
app/src/main/res/layout/card_assign_icon_entry.xml
Normal file
98
app/src/main/res/layout/card_assign_icon_entry.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue