From 4ffe62177f8ca275fc7650bb59448b1b1a29dc76 Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Fri, 1 Dec 2017 15:44:32 +0100 Subject: [PATCH] Automatic re-hide + sort by last used * Automatically hide revealed entries after a (configurable) time * Allow sorting of the list by last used Closes #77 Closes #67 --- .../andotp/Activities/MainActivity.java | 21 +++-- .../andotp/Activities/SettingsActivity.java | 4 +- .../flocke/andotp/Database/Entry.java | 37 +++++++- .../flocke/andotp/Utilities/Settings.java | 6 +- .../andotp/View/EntriesCardAdapter.java | 87 +++++++++++++++++-- .../flocke/andotp/View/EntryViewHolder.java | 64 ++++++++------ .../drawable/ic_sort_inverted_time_white.xml | 24 +++++ app/src/main/res/menu/menu_main.xml | 4 + app/src/main/res/values/settings.xml | 4 + app/src/main/res/values/strings_main.xml | 1 + app/src/main/res/values/strings_settings.xml | 3 + app/src/main/res/xml/preferences.xml | 14 ++- assets/icons/ic_sort_inverted_time.svg | 79 +++++++++++++++++ 13 files changed, 301 insertions(+), 47 deletions(-) create mode 100644 app/src/main/res/drawable/ic_sort_inverted_time_white.xml create mode 100644 assets/icons/ic_sort_inverted_time.svg diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java index 1e30cf1e..c1c2a078 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java @@ -261,10 +261,10 @@ public class MainActivity extends BaseActivity SortMode mode = settings.getSortMode(); adapter.setSortMode(mode); - if (mode == SortMode.LABEL) - touchHelperCallback.setDragEnabled(false); - else + if (mode == SortMode.UNSORTED) touchHelperCallback.setDragEnabled(true); + else + touchHelperCallback.setDragEnabled(false); } } @@ -431,12 +431,12 @@ public class MainActivity extends BaseActivity public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (key.equals(getString(R.string.settings_key_label_size)) || - key.equals(getString(R.string.settings_key_tap_to_reveal)) || key.equals(getString(R.string.settings_key_label_scroll)) || key.equals(getString(R.string.settings_key_thumbnail_visible)) || key.equals(getString(R.string.settings_key_thumbnail_size))) { adapter.notifyDataSetChanged(); - } else if (key.equals(getString(R.string.settings_key_theme)) || + } else if (key.equals(getString(R.string.settings_key_tap_to_reveal)) || + key.equals(getString(R.string.settings_key_theme)) || key.equals(getString(R.string.settings_key_lang)) || key.equals(getString(R.string.settings_key_enable_screenshot))) { recreate(); @@ -503,6 +503,9 @@ public class MainActivity extends BaseActivity } else if (mode == SortMode.LABEL) { sortMenu.setIcon(R.drawable.ic_sort_inverted_label_white); menu.findItem(R.id.menu_sort_label).setChecked(true); + } else if (mode == SortMode.LAST_USED) { + sortMenu.setIcon(R.drawable.ic_sort_inverted_time_white); + menu.findItem(R.id.menu_sort_last_used).setChecked(true); } } @@ -582,6 +585,14 @@ public class MainActivity extends BaseActivity adapter.setSortMode(SortMode.LABEL); touchHelperCallback.setDragEnabled(false); } + } else if (id == R.id.menu_sort_last_used) { + item.setChecked(true); + sortMenu.setIcon(R.drawable.ic_sort_inverted_time_white); + saveSortMode(SortMode.LAST_USED); + if (adapter != null) { + adapter.setSortMode(SortMode.LAST_USED); + touchHelperCallback.setDragEnabled(false); + } } else if (tagsToggle.onOptionsItemSelected(item)) { return true; } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java index 872344b8..89d38066 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java @@ -120,7 +120,7 @@ public class SettingsActivity extends BaseActivity case "password": PasswordHashPreference authPassword = new PasswordHashPreference(getActivity(), null); authPassword.setTitle(R.string.settings_title_auth_password); - authPassword.setOrder(3); + authPassword.setOrder(4); authPassword.setKey(getString(R.string.settings_key_auth_password_hash)); authPassword.setMode(PasswordHashPreference.Mode.PASSWORD); @@ -131,7 +131,7 @@ public class SettingsActivity extends BaseActivity case "pin": PasswordHashPreference authPIN = new PasswordHashPreference(getActivity(), null); authPIN.setTitle(R.string.settings_title_auth_pin); - authPIN.setOrder(3); + authPIN.setOrder(4); authPIN.setKey(getString(R.string.settings_key_auth_pin_hash)); authPIN.setMode(PasswordHashPreference.Mode.PIN); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java b/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java index d647763b..2c1eb332 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java @@ -54,6 +54,7 @@ public class Entry { private static final String JSON_ALGORITHM = "algorithm"; private static final String JSON_TAGS = "tags"; private static final String JSON_THUMBNAIL = "thumbnail"; + private static final String JSON_LAST_USED = "last_used"; private OTPType type = OTPType.TOTP; private int period = TokenCalculator.TOTP_DEFAULT_PERIOD; @@ -62,7 +63,10 @@ public class Entry { private byte[] secret; private String label; private String currentOTP; + private boolean visible = false; + private Runnable hideTask = null; private long last_update = 0; + private long last_used = 0; public List tags = new ArrayList<>(); private EntryThumbnail.EntryThumbnails thumbnail = EntryThumbnail.EntryThumbnails.Default; @@ -164,7 +168,7 @@ public class Entry { this.tags.add(tagsArray.getString(i)); } } catch (Exception e) { - e.printStackTrace(); + // Nothing wrong here } try { @@ -172,6 +176,12 @@ public class Entry { } catch(Exception e) { this.thumbnail = EntryThumbnail.EntryThumbnails.Default; } + + try { + this.last_used = jsonObj.getLong(JSON_LAST_USED); + } catch (Exception e) { + this.last_used = 0; + } } public JSONObject toJSON() throws JSONException { @@ -183,6 +193,7 @@ public class Entry { jsonObj.put(JSON_TYPE, getType().toString()); jsonObj.put(JSON_ALGORITHM, algorithm.toString()); jsonObj.put(JSON_THUMBNAIL, getThumbnail().name()); + jsonObj.put(JSON_LAST_USED, getLastUsed()); JSONArray tagsArray = new JSONArray(); for(String tag : tags){ @@ -253,6 +264,30 @@ public class Entry { return this.period != TokenCalculator.TOTP_DEFAULT_PERIOD; } + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean value) { + this.visible = value; + } + + public void setHideTask(Runnable newTask) { + this.hideTask = newTask; + } + + public Runnable getHideTask() { + return this.hideTask; + } + + public long getLastUsed() { + return this.last_used; + } + + public void setLastUsed(long value) { + this.last_used = value; + } + public String getCurrentOTP() { return currentOTP; } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java index 12996174..fef5a840 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java @@ -53,7 +53,7 @@ public class Settings { } public enum SortMode { - UNSORTED, LABEL + UNSORTED, LABEL, LAST_USED } public Settings(Context context) { @@ -201,6 +201,10 @@ public class Settings { return getBoolean(R.string.settings_key_tap_to_reveal, false); } + public int getTapToRevealTimeout() { + return getInt(R.string.settings_key_tap_to_reveal_timeout, R.integer.settings_default_tap_to_reveal_timeout); + } + public AuthMethod getAuthMethod() { String authString = getString(R.string.settings_key_auth, R.string.settings_default_auth); return AuthMethod.valueOf(authString.toUpperCase()); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java index e1b6c3bd..396ebc82 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java @@ -27,6 +27,7 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; +import android.os.Handler; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.text.Editable; @@ -67,6 +68,7 @@ import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; public class EntriesCardAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter, Filterable { private Context context; + private Handler taskHandler; private EntryFilter filter; private ArrayList entries; private ArrayList displayedEntries; @@ -81,6 +83,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter this.context = context; this.tagsFilterAdapter = tagsFilterAdapter; this.settings = new Settings(context); + this.taskHandler = new Handler(); + loadEntries(); } @@ -156,7 +160,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter public void onBindViewHolder(EntryViewHolder entryViewHolder, int i) { Entry entry = displayedEntries.get(i); - entryViewHolder.updateValues(entry.getLabel(), entry.getCurrentOTP(), entry.getTags(), entry.getThumbnail()); + entryViewHolder.updateValues(entry.getLabel(), entry.getCurrentOTP(), entry.getTags(), entry.getThumbnail(), entry.isVisible()); if (entry.hasNonDefaultPeriod()) { entryViewHolder.showCustomPeriod(entry.getPeriod()); @@ -164,12 +168,6 @@ public class EntriesCardAdapter extends RecyclerView.Adapter entryViewHolder.hideCustomPeriod(); } - if (settings.getTapToReveal()) { - entryViewHolder.enableTapToReveal(); - } else { - entryViewHolder.disableTapToReveal(); - } - entryViewHolder.setLabelSize(settings.getLabelSize()); entryViewHolder.setThumbnailSize(settings.getThumbnailSize()); entryViewHolder.setLabelScroll(settings.getScrollLabel()); @@ -179,7 +177,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter public EntryViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.component_card, viewGroup, false); - EntryViewHolder viewHolder = new EntryViewHolder(context, itemView); + EntryViewHolder viewHolder = new EntryViewHolder(context, itemView, settings.getTapToReveal()); viewHolder.setCallback(new EntryViewHolder.Callback() { @Override public void onMoveEventStart() { @@ -199,14 +197,76 @@ public class EntriesCardAdapter extends RecyclerView.Adapter } @Override - public void onCopyButtonClicked(String text) { + public void onCopyButtonClicked(String text, int position) { copyToClipboard(text); + updateLastUsed(position, getRealIndex(position)); + } + + @Override + public void onTap(final int position) { + if (settings.getTapToReveal()) { + final Entry entry = displayedEntries.get(position); + final int realIndex = entries.indexOf(entry); + + if (entry.isVisible()) { + hideEntry(entry); + } else { + entries.get(realIndex).setHideTask(new Runnable() { + @Override + public void run() { + hideEntry(entry); + } + }); + taskHandler.postDelayed(entries.get(realIndex).getHideTask(), settings.getTapToRevealTimeout() * 1000); + + entry.setVisible(true); + notifyItemChanged(position); + } + } } }); return viewHolder; } + private void hideEntry(Entry entry) { + int pos = displayedEntries.indexOf(entry); + int realIndex = entries.indexOf(entry); + + if (realIndex >= 0) { + entries.get(realIndex).setVisible(false); + taskHandler.removeCallbacks(entries.get(realIndex).getHideTask()); + entries.get(realIndex).setHideTask(null); + } + + boolean updateNeeded = updateLastUsed(pos, realIndex); + + if (pos >= 0) { + displayedEntries.get(pos).setVisible(false); + + if (updateNeeded) + notifyItemChanged(pos); + } + } + + private boolean updateLastUsed(int position, int realIndex) { + long timeStamp = System.currentTimeMillis(); + + if (position >= 0) + displayedEntries.get(position).setLastUsed(timeStamp); + + entries.get(realIndex).setLastUsed(timeStamp); + DatabaseHelper.saveDatabase(context, entries); + + if (sortMode == SortMode.LAST_USED) { + displayedEntries = sortEntries(displayedEntries); + notifyDataSetChanged(); + return false; + } + + return true; + } + @Override public boolean onItemMove(int fromPosition, int toPosition) { if (sortMode == SortMode.UNSORTED && displayedEntries.equals(entries)) { @@ -469,6 +529,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter if (sortMode == SortMode.LABEL) { Collections.sort(sorted, new LabelComparator()); + } else if (sortMode == SortMode.LAST_USED) { + Collections.sort(sorted, new LastUsedComparator()); } return sorted; @@ -538,6 +600,13 @@ public class EntriesCardAdapter extends RecyclerView.Adapter } } + public class LastUsedComparator implements Comparator { + @Override + public int compare(Entry o1, Entry o2) { + return Long.compare(o2.getLastUsed(), o1.getLastUsed()); + } + } + public interface Callback { void onMoveEventStart(); void onMoveEventStop(); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java index a39179c0..68441f87 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java @@ -45,8 +45,8 @@ public class EntryViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder { private Context context; - private Callback callback; + private boolean tapToReveal; private CardView card; private LinearLayout valueLayout; @@ -59,10 +59,11 @@ public class EntryViewHolder extends RecyclerView.ViewHolder private TextView tags; private TextView customPeriod; - public EntryViewHolder(Context context, final View v) { + public EntryViewHolder(Context context, final View v, boolean tapToReveal) { super(v); this.context = context; + this.tapToReveal = tapToReveal; card = v.findViewById(R.id.card_view); value = v.findViewById(R.id.valueText); @@ -100,12 +101,14 @@ public class EntryViewHolder extends RecyclerView.ViewHolder @Override public void onClick(View view) { if (callback != null) - callback.onCopyButtonClicked(value.getText().toString()); + callback.onCopyButtonClicked(value.getText().toString(), getAdapterPosition()); } }); + + setTapToReveal(tapToReveal); } - public void updateValues(String label, String token, List tags, EntryThumbnail.EntryThumbnails thumbnail) { + public void updateValues(String label, String token, List tags, EntryThumbnail.EntryThumbnails thumbnail, boolean isVisible) { Settings settings = new Settings(context); this.label.setText(label); @@ -130,6 +133,18 @@ public class EntryViewHolder extends RecyclerView.ViewHolder int thumbnailSize = settings.getThumbnailSize(); thumbnailImg.setImageBitmap(EntryThumbnail.getThumbnailGraphic(context, label, thumbnailSize, thumbnail)); + + if (this.tapToReveal) { + if (isVisible) { + valueLayout.setVisibility(View.VISIBLE); + coverLayout.setVisibility(View.GONE); + visibleImg.setVisibility(View.GONE); + } else { + valueLayout.setVisibility(View.GONE); + coverLayout.setVisibility(View.VISIBLE); + visibleImg.setVisibility(View.VISIBLE); + } + } } public void showCustomPeriod(int period) { @@ -164,31 +179,25 @@ public class EntryViewHolder extends RecyclerView.ViewHolder } } - public void enableTapToReveal() { - valueLayout.setVisibility(View.GONE); - coverLayout.setVisibility(View.VISIBLE); - visibleImg.setVisibility(View.VISIBLE); + private void setTapToReveal(boolean enabled) { + if (enabled) { + valueLayout.setVisibility(View.GONE); + coverLayout.setVisibility(View.VISIBLE); + visibleImg.setVisibility(View.VISIBLE); - card.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (valueLayout.getVisibility() == View.GONE && coverLayout.getVisibility() == View.VISIBLE) { - valueLayout.setVisibility(View.VISIBLE); - coverLayout.setVisibility(View.GONE); - } else { - valueLayout.setVisibility(View.GONE); - coverLayout.setVisibility(View.VISIBLE); + card.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + callback.onTap(getAdapterPosition()); } - } - }); - } + }); + } else { + valueLayout.setVisibility(View.VISIBLE); + coverLayout.setVisibility(View.GONE); + visibleImg.setVisibility(View.GONE); - public void disableTapToReveal() { - valueLayout.setVisibility(View.VISIBLE); - coverLayout.setVisibility(View.GONE); - visibleImg.setVisibility(View.GONE); - - card.setOnClickListener(null); + card.setOnClickListener(null); + } } @Override @@ -212,6 +221,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder void onMoveEventStop(); void onMenuButtonClicked(View parentView, int position); - void onCopyButtonClicked(String text); + void onCopyButtonClicked(String text, int position); + void onTap(int position); } } diff --git a/app/src/main/res/drawable/ic_sort_inverted_time_white.xml b/app/src/main/res/drawable/ic_sort_inverted_time_white.xml new file mode 100644 index 00000000..82a95301 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_inverted_time_white.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index ff881cef..39dc8bfe 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -17,6 +17,10 @@ + + diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index b8bf263c..bb292886 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -4,6 +4,7 @@ pref_cat_security pref_tap_to_reveal + pref_tap_to_reveal_timeout pref_auth pref_auth_password pref_auth_password_hash @@ -37,6 +38,7 @@ pref_enable_screenshot + 30 none system light @@ -99,6 +101,8 @@ + 5 + 60 12 24 \ No newline at end of file diff --git a/app/src/main/res/values/strings_main.xml b/app/src/main/res/values/strings_main.xml index d7ff70e0..22717af1 100644 --- a/app/src/main/res/values/strings_main.xml +++ b/app/src/main/res/values/strings_main.xml @@ -42,6 +42,7 @@ Unsorted Label + Last used Edit label Change image diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index a5b94bc0..d09bd4b2 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -9,6 +9,7 @@ Tap to reveal + Timeout for tap to reveal Authentication Password PIN @@ -35,6 +36,8 @@ Hide the OTP tokens by default, requiring them to be revealed manually + Select the time (in sec) after which to hide + revealed entries again Decide what happens when a Panic Trigger is received Scroll overlong labels instead of truncating them diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index fd4de0e0..e680e3a2 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -14,9 +14,19 @@ android:summary="@string/settings_desc_tap_to_reveal" android:defaultValue="false" /> + + + + + + + image/svg+xml + + + + + + + + + + + + + +