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
This commit is contained in:
Jakob Nixdorf 2017-12-01 15:44:32 +01:00
parent d10e4a4e35
commit 4ffe62177f
No known key found for this signature in database
GPG key ID: BE99BF86574A7DBC
13 changed files with 301 additions and 47 deletions

View file

@ -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;
}

View file

@ -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);

View file

@ -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<String> 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;
}

View file

@ -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());

View file

@ -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<EntryViewHolder>
implements ItemTouchHelperAdapter, Filterable {
private Context context;
private Handler taskHandler;
private EntryFilter filter;
private ArrayList<Entry> entries;
private ArrayList<Entry> displayedEntries;
@ -81,6 +83,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
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<EntryViewHolder>
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>
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<EntryViewHolder>
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<EntryViewHolder>
}
@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<EntryViewHolder>
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<EntryViewHolder>
}
}
public class LastUsedComparator implements Comparator<Entry> {
@Override
public int compare(Entry o1, Entry o2) {
return Long.compare(o2.getLastUsed(), o1.getLastUsed());
}
}
public interface Callback {
void onMoveEventStart();
void onMoveEventStop();

View file

@ -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<String> tags, EntryThumbnail.EntryThumbnails thumbnail) {
public void updateValues(String label, String token, List<String> 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,7 +179,8 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
}
}
public void enableTapToReveal() {
private void setTapToReveal(boolean enabled) {
if (enabled) {
valueLayout.setVisibility(View.GONE);
coverLayout.setVisibility(View.VISIBLE);
visibleImg.setVisibility(View.VISIBLE);
@ -172,24 +188,17 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
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);
}
callback.onTap(getAdapterPosition());
}
});
}
public void disableTapToReveal() {
} else {
valueLayout.setVisibility(View.VISIBLE);
coverLayout.setVisibility(View.GONE);
visibleImg.setVisibility(View.GONE);
card.setOnClickListener(null);
}
}
@Override
public void onItemSelected() {
@ -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);
}
}

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="m21,18h-6v-2h6zM21,6L21,8L3,8L3,6ZM21,13L9,13v-2h12z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M5.416,18.479m-4.491,0a4.491,4.491 0,1 1,8.981 0a4.491,4.491 0,1 1,-8.981 0"
android:fillAlpha="1"
android:strokeColor="#FFFFFF"
android:fillColor="#00FFFFFF"
android:strokeWidth="1.29999995"
android:strokeAlpha="1"/>
<path
android:pathData="m5.416,15.909v2.57l2.44,2.404"
android:strokeLineCap="butt"
android:strokeColor="#FFFFFF"
android:fillColor="#00FFFFFF"
android:strokeWidth="0.9"
android:strokeLineJoin="miter"
android:strokeAlpha="1"/>
</vector>

View file

@ -17,6 +17,10 @@
<item
android:id="@+id/menu_sort_label"
android:title="@string/menu_sort_label" />
<item
android:id="@+id/menu_sort_last_used"
android:title="@string/menu_sort_last_used" />
</group>
</menu>

View file

@ -4,6 +4,7 @@
<string name="settings_key_cat_security" translatable="false">pref_cat_security</string>
<string name="settings_key_tap_to_reveal" translatable="false">pref_tap_to_reveal</string>
<string name="settings_key_tap_to_reveal_timeout" translatable="false">pref_tap_to_reveal_timeout</string>
<string name="settings_key_auth" translatable="false">pref_auth</string>
<string name="settings_key_auth_password" translatable="false">pref_auth_password</string>
<string name="settings_key_auth_password_hash" translatable="false">pref_auth_password_hash</string>
@ -37,6 +38,7 @@
<string name="settings_key_enable_screenshot" translatable="false">pref_enable_screenshot</string>
<!-- Default values -->
<integer name="settings_default_tap_to_reveal_timeout">30</integer>
<string name="settings_default_auth" translatable="false">none</string>
<string name="settings_default_lang" translatable="false">system</string>
<string name="settings_default_theme" translatable="false">light</string>
@ -99,6 +101,8 @@
</string-array>
<!-- Constraints -->
<integer name="settings_min_tap_to_reveal_timeout">5</integer>
<integer name="settings_max_tap_to_reveal_timeout">60</integer>
<integer name="settings_min_label_size">12</integer>
<integer name="settings_max_label_size">24</integer>
</resources>

View file

@ -42,6 +42,7 @@
<string name="menu_sort_none">Unsorted</string>
<string name="menu_sort_label">Label</string>
<string name="menu_sort_last_used">Last used</string>
<string name="menu_popup_edit_label">Edit label</string>
<string name="menu_popup_change_image">Change image</string>

View file

@ -9,6 +9,7 @@
<!-- Titles -->
<string name="settings_title_tap_to_reveal">Tap to reveal</string>
<string name="settings_title_tap_to_reveal_timeout">Timeout for tap to reveal</string>
<string name="settings_title_auth">Authentication</string>
<string name="settings_title_auth_password">Password</string>
<string name="settings_title_auth_pin">PIN</string>
@ -35,6 +36,8 @@
<!-- Descriptions -->
<string name="settings_desc_tap_to_reveal">Hide the OTP tokens by default, requiring them to be
revealed manually</string>
<string name="settings_desc_tap_to_reveal_timeout">Select the time (in sec) after which to hide
revealed entries again</string>
<string name="settings_desc_panic">Decide what happens when a Panic Trigger is received</string>
<string name="settings_desc_label_scroll">Scroll overlong labels instead of truncating them</string>

View file

@ -14,9 +14,19 @@
android:summary="@string/settings_desc_tap_to_reveal"
android:defaultValue="false" />
<com.vanniktech.vntnumberpickerpreference.VNTNumberPickerPreference
android:key="@string/settings_key_tap_to_reveal_timeout"
android:order="2"
android:title="@string/settings_title_tap_to_reveal_timeout"
android:dialogMessage="@string/settings_desc_tap_to_reveal_timeout"
android:defaultValue="@integer/settings_default_tap_to_reveal_timeout"
android:dependency="@string/settings_key_tap_to_reveal"
app:vnt_minValue="@integer/settings_min_tap_to_reveal_timeout"
app:vnt_maxValue="@integer/settings_max_tap_to_reveal_timeout" />
<ListPreference
android:key="@string/settings_key_auth"
android:order="2"
android:order="3"
android:title="@string/settings_title_auth"
android:summary="%s"
android:entries="@array/settings_entries_auth"
@ -25,7 +35,7 @@
<MultiSelectListPreference
android:key="@string/settings_key_panic"
android:order="4"
android:order="5"
android:title="@string/settings_title_panic"
android:summary="@string/settings_desc_panic"
android:entries="@array/settings_entries_panic"

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
fill="#000000"
height="24"
viewBox="0 0 24 24"
width="24"
version="1.1"
id="svg6"
sodipodi:docname="ic_sort_inverted_time.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1021"
id="namedview8"
showgrid="false"
inkscape:zoom="27.812866"
inkscape:cx="11.117825"
inkscape:cy="9.3826625"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="g824"
inkscape:snap-bbox="true"
inkscape:snap-bbox-midpoints="true"
inkscape:snap-object-midpoints="true" />
<path
d="m 21,18 h -6 v -2 h 6 z M 21,6 V 8 H 3 V 6 Z m 0,7 H 9 v -2 h 12 z"
id="path2"
inkscape:connector-curvature="0" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path4" />
<g
id="g824"
transform="translate(-0.79100081,-18.552564)">
<circle
r="4.4905343"
cy="37.03157"
cx="6.2066832"
id="path815"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.29999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="fill:none;stroke:#000000;stroke-width:0.9;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 6.2066825,34.461271 v 2.570305 l 2.4398741,2.40392"
id="path845"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB