Add tiles view mode

Minor UI improvements
Fix animations
Fix typo
Improvements made after PR review
PR improvements

Co-authored-by: Alexander Bakkker <ab@alexbakker.me>
This commit is contained in:
Michael Schättgen 2023-02-08 02:51:03 +01:00
parent 94a38e82e4
commit d90303cf0e
16 changed files with 461 additions and 107 deletions

View File

@ -5,7 +5,8 @@ import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL;
SMALL,
TILES;
private static ViewMode[] _values;
@ -26,6 +27,8 @@ public enum ViewMode {
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
case TILES:
return R.layout.card_entry_tile;
default:
return R.layout.card_entry;
}
@ -37,8 +40,34 @@ public enum ViewMode {
public float getDividerHeight() {
if (this == ViewMode.COMPACT) {
return 0;
} else if (this == ViewMode.TILES) {
return 4;
}
return 20;
}
public int getColumnSpan() {
if (this == ViewMode.TILES) {
return 2;
}
return 1;
}
public float getDividerWidth() {
if (this == ViewMode.TILES) {
return 4;
}
return 0;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
}
return String.format("(%s)", accountName);
}
}

View File

@ -16,6 +16,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final EntryAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
_adapter = adapter;
@ -46,6 +47,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
return false;
}
public void setDragFlags(int dragFlags) {
_dragFlags = dragFlags;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// It's not clear when this can happen, but sometimes the ViewHolder
@ -57,16 +62,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
}
int swipeFlags = 0;
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
if (adapter.isPositionFooter(position)
|| adapter.getEntryAt(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
dragFlags = 0;
return makeMovementFlags(0, swipeFlags);
}
return makeMovementFlags(dragFlags, swipeFlags);
return makeMovementFlags(_dragFlags, swipeFlags);
}
@Override
@ -75,7 +79,11 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
if (target.getAdapterPosition() < _adapter.getShownFavoritesCount()){
return false;
}
_adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
int firstPosition = viewHolder.getLayoutPosition();
int secondPosition = target.getAdapterPosition();
_adapter.onItemMove(firstPosition, secondPosition);
_positionChanged = true;
return true;
}
@ -92,6 +100,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getAdapterPosition());
_positionChanged = false;
_adapter.refresh(false);
}
}
}

View File

@ -952,7 +952,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_vaultManager.getVault().swapEntries(entry1, entry2);
_vaultManager.getVault().moveEntry(entry1, entry2);
}
@Override

View File

@ -17,6 +17,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
public class AppearancePreferencesFragment extends PreferencesFragment {
private Preference _groupsPreference;
private Preference _resetUsageCountPreference;
private Preference _currentAccountNamePositionPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@ -89,6 +90,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
_prefs.setCurrentViewMode(ViewMode.fromInteger(i));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
getResult().putExtra("needsRefresh", true);
overrideAccountNamePosition(ViewMode.fromInteger(i) == ViewMode.TILES);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
@ -110,9 +112,9 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
});
int currentAccountNamePosition = _prefs.getAccountNamePosition().ordinal();
Preference currentAccountNamePositionPreference = requirePreference("pref_account_name_position");
currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition]));
currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
_currentAccountNamePositionPreference = requirePreference("pref_account_name_position");
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition]));
_currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
int currentAccountNamePosition1 = _prefs.getAccountNamePosition().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
@ -120,7 +122,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
.setSingleChoiceItems(R.array.account_name_position_titles, currentAccountNamePosition1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setAccountNamePosition(AccountNamePosition.fromInteger(i));
currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i]));
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i]));
getResult().putExtra("needsRefresh", true);
dialog.dismiss();
})
@ -135,5 +137,17 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
getResult().putExtra("needsRefresh", true);
return true;
});
overrideAccountNamePosition(_prefs.getCurrentViewMode() == ViewMode.TILES);
}
private void overrideAccountNamePosition(boolean override) {
if (override) {
_currentAccountNamePositionPreference.setEnabled(false);
_currentAccountNamePositionPreference.setSummary(getString(R.string.pref_account_name_position_summary_override));
} else {
_currentAccountNamePositionPreference.setEnabled(true);
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()]));
}
}
}

View File

@ -29,6 +29,7 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.CollectionUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.ArrayList;
@ -382,9 +383,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
// notify the vault first
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
Collections.swap(_entries, firstPosition, secondPosition);
Collections.swap(_shownEntries, firstPosition, secondPosition);
// then update our end
CollectionUtils.move(_entries, firstPosition, secondPosition);
CollectionUtils.move(_shownEntries, firstPosition, secondPosition);
notifyItemMoved(firstPosition, secondPosition);
}
@ -438,7 +440,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN;
entryHolder.setData(entry, _codeGroupSize, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed);
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed);
entryHolder.setFocused(_selectedEntries.contains(entry));
entryHolder.setShowDragHandle(isEntryDraggable(entry));
@ -467,7 +469,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
case SINGLETAP:
if (!handled) {
_view.onEntryCopy(entry);
entryHolder.animateCopyText();
entryHolder.animateCopyText(_viewMode != ViewMode.TILES);
_clickedEntry = null;
}
break;
@ -476,7 +478,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
if(entry == _clickedEntry) {
_view.onEntryCopy(entry);
entryHolder.animateCopyText();
entryHolder.animateCopyText(_viewMode != ViewMode.TILES);
_clickedEntry = null;
} else {
_clickedEntry = entry;

View File

@ -16,6 +16,7 @@ import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
@ -46,6 +47,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private ImageView _buttonRefresh;
private RelativeLayout _description;
private ImageView _dragHandle;
private ViewMode _viewMode;
private final ImageView _selected;
private final Handler _selectedHandler;
@ -107,11 +109,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
});
}
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) {
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) {
_entry = entry;
_hidden = hidden;
_paused = paused;
_codeGrouping = groupSize;
_viewMode = viewMode;
_accountNamePosition = accountNamePosition;
_selected.clearAnimation();
@ -129,12 +132,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
String profileIssuer = entry.getIssuer();
String profileName = entry.getName();
if (!profileIssuer.isEmpty() && !profileName.isEmpty() && accountNamePosition == AccountNamePosition.END) {
profileName = String.format(" (%s)", profileName);
if (!profileIssuer.isEmpty() && !profileName.isEmpty() && _accountNamePosition == AccountNamePosition.END) {
profileName = _viewMode.getFormattedAccountName(profileName);
}
_profileIssuer.setText(profileIssuer);
_profileName.setText(profileName);
setAccountNameLayout(accountNamePosition);
setAccountNameLayout(_accountNamePosition);
if (_hidden) {
hideCode();
@ -148,6 +151,10 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
private void setAccountNameLayout(AccountNamePosition accountNamePosition) {
if (_viewMode == ViewMode.TILES) {
return;
}
RelativeLayout.LayoutParams profileNameLayoutParams;
RelativeLayout.LayoutParams copiedLayoutParams;
switch (accountNamePosition) {
@ -367,7 +374,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
animateAlphaTo(DEFAULT_ALPHA);
}
public void animateCopyText() {
public void animateCopyText(boolean includeSlideAnimation) {
_animationHandler.removeCallbacksAndMessages(null);
Animation slideDownFadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
@ -375,16 +382,25 @@ public class EntryHolder extends RecyclerView.ViewHolder {
Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out);
Animation fadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_in);
_profileCopied.startAnimation(slideDownFadeIn);
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
if (includeSlideAnimation) {
_profileCopied.startAnimation(slideDownFadeIn);
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
fadeOutView.startAnimation(slideDownFadeOut);
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
fadeOutView.startAnimation(fadeIn);
}, 3000);
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
fadeOutView.startAnimation(fadeIn);
}, 3000);
} else {
_profileCopied.startAnimation(fadeIn);
_profileName.startAnimation(fadeOut);
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
_profileName.startAnimation(fadeIn);
}, 3000);
}
}
private void animateAlphaTo(float alpha) {

View File

@ -19,6 +19,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -64,7 +65,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private ItemTouchHelper _touchHelper;
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private RecyclerView.ItemDecoration _verticalDividerDecoration;
private RecyclerView.ItemDecoration _horizontalDividerDecoration;
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
private TotpProgressBar _progressBar;
private boolean _showProgress;
@ -122,7 +124,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
RecyclerViewPreloader<VaultEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
_recyclerView.addOnScrollListener(preloader);
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
GridLayoutManager layoutManager = new GridLayoutManager(requireContext(), 1);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (_viewMode == ViewMode.TILES && position == _adapter.getEntriesCount()) {
return 2;
}
return 1;
}
});
_recyclerView.setLayoutManager(layoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
_touchHelper = new ItemTouchHelper(_touchCallback);
@ -222,6 +234,13 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_viewMode = mode;
updateDividerDecoration();
_adapter.setViewMode(_viewMode);
if (_viewMode == ViewMode.TILES) {
_touchCallback.setDragFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
} else {
_touchCallback.setDragFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN);
}
((GridLayoutManager)_recyclerView.getLayoutManager()).setSpanCount(mode.getColumnSpan());
}
public void startDrag(RecyclerView.ViewHolder viewHolder) {
@ -536,18 +555,28 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
private void updateDividerDecoration() {
if (_dividerDecoration != null) {
_recyclerView.removeItemDecoration(_dividerDecoration);
if (_verticalDividerDecoration != null) {
_recyclerView.removeItemDecoration(_verticalDividerDecoration);
}
if(_horizontalDividerDecoration != null) {
_recyclerView.removeItemDecoration(_horizontalDividerDecoration);
}
float height = _viewMode.getDividerHeight();
float width = _viewMode.getDividerWidth();
if (_showProgress && height == 0) {
_dividerDecoration = new CompactDividerDecoration();
_verticalDividerDecoration = new CompactDividerDecoration();
} else {
_dividerDecoration = new VerticalSpaceItemDecoration(height);
_verticalDividerDecoration = new VerticalSpaceItemDecoration(height);
}
_recyclerView.addItemDecoration(_dividerDecoration);
if (width != 0) {
_horizontalDividerDecoration = new TileSpaceItemDecoration(width, height);
_recyclerView.addItemDecoration(_horizontalDividerDecoration);
} else {
_recyclerView.addItemDecoration(_verticalDividerDecoration);
}
}
private void updateEmptyState() {
@ -653,6 +682,30 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
private class TileSpaceItemDecoration extends RecyclerView.ItemDecoration {
private final int _width;
private final int _height;
private TileSpaceItemDecoration(float width, float height) {
// convert dp to pixels
_width = MetricsHelper.convertDpToPixels(requireContext(), width);
_height = MetricsHelper.convertDpToPixels(requireContext(), height);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int adapterPosition = parent.getChildAdapterPosition(view);
if (adapterPosition == NO_POSITION) {
return;
}
outRect.left = _width;
outRect.right = _width;
outRect.top = _height;
outRect.bottom = _height;
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
@NonNull
@Override

View File

@ -0,0 +1,15 @@
package com.beemdevelopment.aegis.util;
import java.util.List;
public class CollectionUtils {
public static <T> void move(List<T> list, int fromIndex, int toIndex) {
if (fromIndex == toIndex) {
return;
}
T item = list.remove(fromIndex);
list.add(toIndex, item);
}
}

View File

@ -64,34 +64,31 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
}
/**
* Swaps the position of value1 and value2 in the internal map. This operation is
* quite expensive because it has to reallocate the entire underlying LinkedHashMap.
* @throws AssertionError if no map value exists with the UUID of the given entries.
*/
public void swap(T value1, T value2) {
boolean found1 = false;
boolean found2 = false;
List<T> values = new ArrayList<>();
* Moves value1 to the position of value2.
*/
public void move(T value1, T value2) {
List<T> values = new ArrayList<>(_map.values());
for (T value : _map.values()) {
int vi1 = -1, vi2 = -1;
for (int i = 0; i < values.size(); i++) {
T value = values.get(i);
if (value.getUUID().equals(value1.getUUID())) {
values.add(value2);
found1 = true;
} else if (value.getUUID().equals(value2.getUUID())) {
values.add(value1);
found2 = true;
} else {
values.add(value);
vi1 = i;
}
if (value.getUUID().equals(value2.getUUID())) {
vi2 = i;
}
}
if (!found1) {
if (vi1 < 0) {
throw new AssertionError(String.format("No value found for value1 with UUID: %s", value1.getUUID()));
}
if (!found2) {
if (vi2 < 0) {
throw new AssertionError(String.format("No value found for value2 with UUID: %s", value2.getUUID()));
}
CollectionUtils.move(values, vi1, vi2);
_map.clear();
for (T value : values) {
_map.put(value.getUUID(), value);

View File

@ -239,8 +239,11 @@ public class VaultRepository {
return _vault.getEntries().replace(entry);
}
public void swapEntries(VaultEntry entry1, VaultEntry entry2) {
_vault.getEntries().swap(entry1, entry2);
/**
* Moves entry1 to the position of entry2.
*/
public void moveEntry(VaultEntry entry1, VaultEntry entry2) {
_vault.getEntries().move(entry1, entry2);
}
public boolean isEntryDuplicate(VaultEntry entry) {

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillEnabled="true">
<translate
android:duration="0"
android:fromYDelta="-100%"

View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillEnabled="true">
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="300"/>
</set>

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
app:cardCornerRadius="6dp"
android:clipChildren="true">
<LinearLayout
android:orientation="horizontal"
android:id="@+id/rlCardEntry"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/favorite_indicator"
android:layout_width="15dp"
android:layout_height="match_parent"
android:layout_marginStart="-11dp"
android:backgroundTint="@color/colorFavorite"
android:background="@drawable/button_rounded_corners" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="bottom"
android:id="@+id/relativeLayout"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingStart="8dp">
<RelativeLayout
android:id="@+id/layoutImage"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivTextDrawable"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="6dp"
android:layout_alignParentStart="true" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivSelected"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="6dp"
android:layout_alignParentStart="true"
android:src="@drawable/item_selected"
android:visibility="gone"
app:civ_circle_background_color="@color/colorPrimarySelected" />
</RelativeLayout>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_toEndOf="@+id/layoutImage">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_issuer"
android:text="@string/issuer"
android:textColor="?attr/primaryText"
android:textStyle="bold"
android:includeFontPadding="false"
android:textSize="11sp"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/profile_copied"
android:layout_width="wrap_content"
android:layout_below="@id/profile_issuer"
android:layout_height="wrap_content"
android:maxLines="1"
android:includeFontPadding="false"
android:visibility="invisible"
android:text="@string/copied"
android:textColor="?attr/secondaryText"
android:textSize="9sp" />
<TextView
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_issuer"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/extra_info_text"
android:textSize="9sp"
tools:text=" - AccountName" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="26sp"
android:textColor="?attr/codePrimaryText"
android:layout_alignParentStart="true"
android:layout_marginTop="10dp"
android:textStyle="normal|bold"/>
</RelativeLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<ImageView
android:id="@+id/buttonRefresh"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="0dp"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_refresh_black_24dp"
app:tint="?attr/iconColorPrimary"
android:background="?android:attr/selectableItemBackground" />
<ImageView
android:id="@+id/drag_handle"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="-12dp"
android:visibility="invisible"
android:scaleType="fitXY"
android:src="@drawable/ic_baseline_menu_black_32" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:padding="0dp"
android:layout_margin="0dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.beemdevelopment.aegis.ui.views.TotpProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="3dp"
android:id="@+id/progressBar"
android:max="5000"
android:layout_weight="1"/>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -32,6 +32,7 @@
<item>@string/normal_viewmode_title</item>
<item>@string/compact_mode_title</item>
<item>@string/small_mode_title</item>
<item>@string/tiles_mode_title</item>
</string-array>
<string-array name="pref_lang_entries">

View File

@ -45,6 +45,7 @@
<string name="pref_account_name_position_title">Show the account name</string>
<string name="pref_shared_issuer_account_name_title">Only show account name when necessary</string>
<string name="pref_shared_issuer_account_name_summary">Only show account names whenever they share the same issuer. Other account names will be hidden.</string>
<string name="pref_account_name_position_summary_override">This setting is overridden by the tiles view mode. Account name will always be shown below the issuer.</string>
<string name="pref_import_file_title">Import from file</string>
<string name="pref_import_file_summary">Import tokens from a file</string>
<string name="pref_android_backups_title">Android cloud backups</string>
@ -329,6 +330,7 @@
<string name="normal_viewmode_title">Normal</string>
<string name="compact_mode_title">Compact</string>
<string name="small_mode_title">Small</string>
<string name="tiles_mode_title">Tiles</string>
<string name="unknown_issuer">Unknown issuer</string>
<string name="unknown_account_name">Unknown account name</string>
<plurals name="import_error_dialog">

View File

@ -1,103 +1,135 @@
package com.beemdevelopment.aegis.util;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class UUIDMapTest {
private UUIDMap<Value> _map;
@Before
public void init() {
_map = new UUIDMap<>();
}
@Test
public void addValue() {
// try adding a new value
Value value = addNewValue();
UUIDMap<Value> map = new UUIDMap<>();
Value value = addNewValue(map);
// try re-adding the value
assertThrows(AssertionError.class, () -> _map.add(value));
assertThrows(AssertionError.class, () -> map.add(value));
// try adding a clone of the value
assertThrows(AssertionError.class, () -> _map.add(Cloner.clone(value)));
assertThrows(AssertionError.class, () -> map.add(Cloner.clone(value)));
}
@Test
public void removeValue() {
// try removing a value
final Value value = addNewValue();
Value oldValue = _map.remove(value);
assertFalse(_map.has(value));
UUIDMap<Value> map = new UUIDMap<>();
final Value value = addNewValue(map);
Value oldValue = map.remove(value);
assertFalse(map.has(value));
// ensure we got the original value back
assertEquals(value, oldValue);
// try removing a non-existent value
assertThrows(AssertionError.class, () -> _map.remove(value));
assertThrows(AssertionError.class, () -> map.remove(value));
// try removing a value using a clone
Value value2 = addNewValue();
_map.remove(Cloner.clone(value2));
assertFalse(_map.has(value2));
Value value2 = addNewValue(map);
map.remove(Cloner.clone(value2));
assertFalse(map.has(value2));
}
@Test
public void replaceValue() {
Value value = addNewValue();
UUIDMap<Value> map = new UUIDMap<>();
Value value = addNewValue(map);
// replace the value with a clone
Value valueClone = Cloner.clone(value);
Value oldValue = _map.replace(valueClone);
Value oldValue = map.replace(valueClone);
// ensure we got the original value back
assertEquals(value, oldValue);
// ensure that the clone is now stored in the map
assertSame(_map.getByUUID(value.getUUID()), valueClone);
assertSame(map.getByUUID(value.getUUID()), valueClone);
}
@Test
public void swapValue() {
Collection<Value> values = _map.getValues();
public void moveValue() {
// move the first value to the last value
UUIDMap<Value> map = fillNewMap(4);
Value[] values = map.getValues().toArray(new Value[0]);
map.move(values[0], values[3]);
assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{
values[1],
values[2],
values[3],
values[0]
});
// set up the map with some values
Value value1 = addNewValue();
Value value2 = addNewValue();
Value value3 = addNewValue();
Value value4 = addNewValue();
// move the last value to the first value
map = fillNewMap(4);
values = map.getValues().toArray(new Value[0]);
map.move(values[3], values[0]);
assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{
values[3],
values[0],
values[1],
values[2]
});
// set up a reference list with the reverse order
List<Value> ref = new ArrayList<>(values);
Collections.reverse(ref);
// move the second value to the third value
map = fillNewMap(4);
values = map.getValues().toArray(new Value[0]);
map.move(values[1], values[2]);
assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{
values[0],
values[2],
values[1],
values[3]
});
// the lists should not be equal at this point
assertNotEquals(values, ref);
// move the third value to the second value
map = fillNewMap(4);
values = map.getValues().toArray(new Value[0]);
map.move(values[2], values[1]);
assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{
values[0],
values[2],
values[1],
values[3]
});
// swap the values and see if the lists are equal now
_map.swap(value1, value4);
_map.swap(value2, value3);
assertArrayEquals(values.toArray(), ref.toArray());
// move the third value to the first value
map = fillNewMap(4);
values = map.getValues().toArray(new Value[0]);
map.move(values[2], values[0]);
assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{
values[2],
values[0],
values[1],
values[3]
});
}
private Value addNewValue() {
private UUIDMap<Value> fillNewMap(int n) {
UUIDMap<Value> map = new UUIDMap<>();
for (int i = 0; i < n; i++) {
addNewValue(map);
}
return map;
}
private Value addNewValue(UUIDMap<Value> map) {
Value value = new Value();
assertFalse(_map.has(value));
_map.add(value);
assertTrue(_map.has(value));
assertFalse(map.has(value));
map.add(value);
assertTrue(map.has(value));
return value;
}