Merge pull request #1006 from beemdevelopment/feature/favorites

Add ability to pin/favorite entries
This commit is contained in:
Alexander Bakker 2022-10-26 14:14:51 +02:00 committed by GitHub
commit 031a11250a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 247 additions and 20 deletions

View file

@ -1,6 +1,7 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
@ -9,20 +10,27 @@ import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import androidx.annotation.IdRes;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Hex;
@ -127,8 +135,9 @@ public class OverallTest extends AegisTest {
onView(withId(R.id.action_share_qr)).perform(click());
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
onView(withId(R.id.action_delete)).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
onView(withText(R.string.action_delete)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
openContextualActionModeOverflowMenu();

View file

@ -20,9 +20,11 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class Preferences {
@ -233,6 +235,27 @@ public class Preferences {
_prefs.edit().putString("pref_usage_count", usageCountJson.toString()).apply();
}
public List<UUID> getFavorites() {
List<UUID> favorites = new ArrayList<>();
Set<String> favoritesStringSet = _prefs.getStringSet("pref_favorites", null);
if(favoritesStringSet != null) {
for (String favorite : favoritesStringSet) {
favorites.add(UUID.fromString(favorite));
}
}
return favorites;
}
public void setFavorites(List<UUID> favorites) {
Set<String> favoritesHashSet = new HashSet<String>();
for (UUID favorite : favorites) {
favoritesHashSet.add(favorite.toString());
}
_prefs.edit().putStringSet("pref_favorites", favoritesHashSet).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}

View file

@ -10,11 +10,11 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private VaultEntry _selectedEntry;
private final ItemTouchHelperAdapter _adapter;
private final EntryAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
_adapter = adapter;
}
@ -28,7 +28,14 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
}
public void setSelectedEntry(VaultEntry entry) {
_selectedEntry = entry;
if (entry == null) {
_selectedEntry = null;
return;
}
if (!entry.getIsFavorited()) {
_selectedEntry = entry;
}
}
@Override
@ -41,13 +48,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = 0;
int position = viewHolder.getAdapterPosition();
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
if (adapter.isPositionFooter(position)
if (viewHolder != null) {
int position = viewHolder.getAdapterPosition();
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
if (adapter.isPositionFooter(position)
|| adapter.getEntryAt(position) != _selectedEntry
|| !isLongPressDragEnabled())
{
dragFlags = 0;
{
dragFlags = 0;
}
}
return makeMovementFlags(dragFlags, swipeFlags);
@ -56,6 +65,9 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
if(target.getAdapterPosition() < _adapter.getFavorites().size()){
return false;
}
_adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
_positionChanged = true;
return true;

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class FavoriteComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return -1 * Boolean.compare(a.getIsFavorited(), b.getIsFavorited());
}
}

View file

@ -110,7 +110,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_isDPadPressed = false;
_isDoingIntro = false;
_isAuthenticating = false;
if (savedInstanceState != null) {
_isRecreated = true;
_pendingSearchQuery = savedInstanceState.getString("pendingSearchQuery");
@ -177,10 +176,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
protected void onPause() {
Map<UUID, Integer> usageMap = _entryListView.getUsageCounts();
List<UUID> favoritesList = _entryListView.getFavorites();
if (usageMap != null) {
_prefs.setUsageCount(usageMap);
}
if (favoritesList != null) {
_prefs.setFavorites(favoritesList);
}
super.onPause();
}
@ -645,6 +649,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_main, menu);
updateLockIcon();
if (_loaded) {
_entryListView.setGroups(_vaultManager.getVault().getGroups());
@ -774,6 +779,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void loadEntries() {
if (!_loaded) {
_entryListView.setUsageCounts(_prefs.getUsageCounts());
_entryListView.setFavorites(_prefs.getFavorites());
_entryListView.addEntries(_vaultManager.getVault().getEntries());
_entryListView.runEntriesAnimation();
_loaded = true;
@ -859,6 +865,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (_selectedEntries.isEmpty()) {
_actionMode.finish();
} else {
setFavoriteMenuItemVisiblity();
setIsMultipleSelected(_selectedEntries.size() > 1);
}
@ -882,6 +889,27 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected);
}
private void setFavoriteMenuItemVisiblity() {
MenuItem toggleFavoriteMenuItem = _actionMode.getMenu().findItem(R.id.action_toggle_favorite);
if (_selectedEntries.size() == 1){
if (_selectedEntries.get(0).getIsFavorited()) {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_set_favorite);
toggleFavoriteMenuItem.setTitle(R.string.unfavorite);
} else {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_unset_favorite);
toggleFavoriteMenuItem.setTitle(R.string.favorite);
}
} else {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_unset_favorite);
toggleFavoriteMenuItem.setTitle(String.format("%s / %s", getString(R.string.favorite), getString(R.string.unfavorite)));
}
}
private void toggleFavorite(VaultEntry entry) {
_entryListView.toggleFavoriteState(entry);
}
@Override
public void onLongEntryClick(VaultEntry entry) {
if (!_selectedEntries.isEmpty()) {
@ -896,6 +924,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void startActionMode() {
_actionMode = startSupportActionMode(_actionModeCallbacks);
_actionModeBackPressHandler.setEnabled(true);
setFavoriteMenuItemVisiblity();
}
@Override
@ -1021,7 +1050,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private class ActionModeCallbacks implements ActionMode.Callback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_action_mode, menu);
return true;
}
@ -1048,6 +1077,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
mode.finish();
return true;
case R.id.action_toggle_favorite:
for (VaultEntry entry : _selectedEntries) {
toggleFavorite(entry);
}
mode.finish();
return true;
case R.id.action_share_qr:
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();

View file

@ -19,6 +19,7 @@ import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
import com.beemdevelopment.aegis.helpers.comparators.FavoriteComparator;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@ -41,6 +42,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
private List<VaultEntry> _shownEntries;
private List<VaultEntry> _selectedEntries;
private Map<UUID, Integer> _usageCounts;
private List<UUID> _favorites;
private VaultEntry _focusedEntry;
private Preferences.CodeGrouping _codeGroupSize;
private boolean _showAccountName;
@ -111,6 +113,17 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
_pauseFocused = pauseFocused;
}
public void toggleFavoriteState(VaultEntry entry) {
if (_favorites.contains(entry.getUUID())) {
_favorites.remove(entry.getUUID());
} else {
_favorites.add(entry.getUUID());
}
entry.setIsFavorited(_favorites.contains(entry.getUUID()));
updateShownEntries();
}
public VaultEntry getEntryAt(int position) {
return _shownEntries.get(position);
}
@ -126,7 +139,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
if (comparator != null) {
// insert the entry in the correct order
// note: this assumes that _shownEntries has already been sorted
for (int i = 0; i < _shownEntries.size(); i++) {
for (int i = _favorites.size(); i < _shownEntries.size(); i++) {
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
_shownEntries.add(i, entry);
notifyItemInserted(i);
@ -156,6 +169,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public void addEntries(Collection<VaultEntry> entries) {
for (VaultEntry entry: entries) {
entry.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0);
entry.setIsFavorited(_favorites.contains(entry.getUUID()));
}
_entries.addAll(entries);
@ -166,6 +180,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public void removeEntry(VaultEntry entry) {
_entries.remove(entry);
if (_favorites.contains(entry.getUUID())) {
removeFavorite(entry);
}
if (_shownEntries.contains(entry)) {
int position = _shownEntries.indexOf(entry);
_shownEntries.remove(position);
@ -303,6 +321,9 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
Collections.sort(_shownEntries, comparator);
}
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
Collections.sort(_shownEntries, favoriteComparator);
_view.onListChange();
notifyDataSetChanged();
}
@ -315,6 +336,21 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public Map<UUID, Integer> getUsageCounts() { return _usageCounts; }
public void setFavorites(List<UUID> favorites) { _favorites = favorites; }
public List<UUID> getFavorites() { return _favorites; }
public void removeFavorite(VaultEntry entry) {
int position = -1;
for (int i = 0; i < _favorites.size(); i++) {
if (_favorites.get(i).equals(entry.getUUID())) {
position = i;
}
}
_favorites.remove(position);
}
public void setGroups(TreeSet<String> groups) {
_view.setGroups(groups);
}
@ -449,7 +485,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
boolean dragEnabled = _selectedEntries.size() == 0
|| _selectedEntries.size() == 1 && _selectedEntries.get(0) == entryHolder.getEntry();
if (dragEnabled && isDragAndDropAllowed()) {
if (dragEnabled && isDragAndDropAllowed() && !entryHolder.getEntry().getIsFavorited()) {
_view.startDrag(_dragHandleHolder);
}
@ -463,7 +499,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
if (event.getActionMasked() == MotionEvent.ACTION_MOVE
&& _selectedEntries.size() == 1
&& _selectedEntries.get(0) == entryHolder.getEntry()
&& isDragAndDropAllowed()) {
&& isDragAndDropAllowed()
&& !entryHolder.getEntry().getIsFavorited()) {
_view.startDrag(_dragHandleHolder);
return true;
}
@ -607,7 +644,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
return;
}
if (_selectedEntries.size() == 1 && _dragHandleHolder == null) {
if (_selectedEntries.size() == 1 && _dragHandleHolder == null && !_selectedEntries.get(0).getIsFavorited()) {
// Find and enable dragging for the single selected EntryHolder
// Not nice but this is the best method I could find
for (int i = 0; i < _holders.size(); i++) {

View file

@ -35,6 +35,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DIMMED_ALPHA = 0.2f;
private static final char HIDDEN_CHAR = '●';
private View _favoriteIndicator;
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
@ -76,6 +77,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
_selected = view.findViewById(R.id.ivSelected);
_dragHandle = view.findViewById(R.id.drag_handle);
_favoriteIndicator = view.findViewById(R.id.favorite_indicator);
_selectedHandler = new Handler();
_animationHandler = new Handler();
@ -114,6 +116,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_selectedHandler.removeCallbacksAndMessages(null);
_animationHandler.removeCallbacksAndMessages(null);
_favoriteIndicator.setVisibility(_entry.getIsFavorited() ? View.VISIBLE : View.INVISIBLE);
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
setShowProgress(showProgress);

View file

@ -197,6 +197,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
return _adapter.getUsageCounts();
}
public void setFavorites(List<UUID> favorites) {
_adapter.setFavorites(favorites);
}
public List<UUID> getFavorites() { return _adapter.getFavorites(); }
public void setSearchFilter(String search) {
_adapter.setSearchFilter(search);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
@ -350,6 +356,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
public void toggleFavoriteState(VaultEntry entry) {
_adapter.toggleFavoriteState(entry);
}
public void tempHighlightEntry(VaultEntry entry) {
_adapter.setTempHighlightEntry(true);
@ -543,7 +553,23 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
// the first item should also have a top margin
outRect.top = _height;
}
outRect.bottom = _height;
int adapterPosition = parent.getChildAdapterPosition(view);
if (adapterPosition == -1) {
return;
}
if (adapterPosition < _adapter.getEntriesCount() && _adapter.getEntryAt(adapterPosition).getIsFavorited()) {
if (_adapter.getFavorites().size() == parent.getChildAdapterPosition(view) + 1) {
outRect.bottom = MetricsHelper.convertDpToPixels(requireContext(), 20);
return;
}
outRect.bottom = 0;
return;
}
outRect.top = _height;
}
}

View file

@ -26,6 +26,7 @@ public class VaultEntry extends UUIDMap.Value {
private OtpInfo _info;
private byte[] _icon;
private IconType _iconType = IconType.INVALID;
private boolean _isFavorited;
private int _usageCount;
private String _note = "";
@ -142,6 +143,8 @@ public class VaultEntry extends UUIDMap.Value {
public String getNote() { return _note; }
public boolean getIsFavorited() { return _isFavorited; };
public void setName(String name) {
_name = name;
}
@ -171,6 +174,8 @@ public class VaultEntry extends UUIDMap.Value {
public void setNote(String note) { _note = note; }
public void setIsFavorited(boolean isFavorited) { _isFavorited = isFavorited; }
@Override
public boolean equals(Object o) {
if (!(o instanceof VaultEntry)) {

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.1111112"
android:scaleY="1.1111112"
android:translateX="-1.3333334"
android:translateY="-1.3333334">
<path
android:fillColor="@android:color/white"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</group>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.1111112"
android:scaleY="1.1111112"
android:translateX="-1.3333334"
android:translateY="-1.3333334">
<path
android:fillColor="@android:color/white"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
</group>
</vector>

View file

@ -18,6 +18,14 @@
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="wrap_content"
android:layout_height="match_parent"

View file

@ -18,6 +18,14 @@
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="wrap_content"
android:layout_height="match_parent"
@ -27,9 +35,8 @@
android:id="@+id/ivTextDrawable"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
/>
android:layout_centerVertical="true" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivSelected"

View file

@ -18,6 +18,14 @@
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="wrap_content"
android:layout_height="match_parent"

View file

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_toggle_favorite"
android:orderInCategory="70"
android:icon="@drawable/ic_unset_favorite"
android:title="@string/favorite"
app:showAsAction="always" />
<item
android:id="@+id/action_copy"
android:orderInCategory="80"
@ -21,7 +27,7 @@
android:orderInCategory="100"
android:icon="@drawable/ic_delete_white"
android:tint="?attr/iconColorPrimary"
app:showAsAction="always"/>
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_share_qr"

View file

@ -8,6 +8,7 @@
<color name="colorPrimaryLight">#5472d3</color>
<color name="colorAccent">#FF5252</color>
<color name="colorAccentPressed">#FF5252</color>
<color name="colorFavorite">#F9A825</color>
<color name="primary_text">#212121</color>
<color name="secondary_text">#A0A0A0</color>
<color name="auth_text">#4c4c4c</color>

View file

@ -152,6 +152,8 @@
<string name="set_up_biometric">Set up biometric unlock</string>
<string name="copy">Copy</string>
<string name="edit">Edit</string>
<string name="favorite">Favorite</string>
<string name="unfavorite">Unfavorite</string>
<string name="error_all_caps">ERROR</string>
<string name="password">Password</string>
<string name="confirm_password">Confirm password</string>