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 b693a9cd..4c107ea3 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
@@ -491,7 +491,8 @@ public class MainActivity extends BaseActivity
key.equals(getString(R.string.settings_key_theme_mode)) ||
key.equals(getString(R.string.settings_key_theme_black_auto)) ||
key.equals(getString(R.string.settings_key_hide_global_timeout)) ||
- key.equals(getString(R.string.settings_key_hide_issuer))) {
+ key.equals(getString(R.string.settings_key_hide_issuer)) ||
+ key.equals(getString(R.string.settings_key_show_prev_token))) {
recreateActivity = true;
}
}
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 63b52dc7..545b3c71 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
@@ -70,6 +70,7 @@ public class Entry {
private String issuer;
private String label;
private String currentOTP;
+ private String prevOTP;
private boolean visible = false;
private Runnable hideTask = null;
private long last_update = 0;
@@ -150,7 +151,12 @@ public class Entry {
String counter = uri.getQueryParameter("counter");
String issuer = uri.getQueryParameter("issuer");
- String label = getStrippedLabel(issuer, uri.getPath().substring(1));
+
+ String label = "";
+
+ if (uri.getPath() != null)
+ label = getStrippedLabel(issuer, uri.getPath().substring(1));
+
String period = uri.getQueryParameter("period");
String digits = uri.getQueryParameter("digits");
String algorithm = uri.getQueryParameter("algorithm");
@@ -172,6 +178,10 @@ public class Entry {
this.issuer = issuer;
this.label = label;
+
+ if (secret == null)
+ throw new Exception("Empty secret");
+
if(type == OTPType.MOTP) {
this.secret = secret.getBytes();
} else {
@@ -475,6 +485,10 @@ public class Entry {
return currentOTP;
}
+ public String getPrevOTP() {
+ return prevOTP;
+ }
+
public String getPin() {
return pin;
}
@@ -492,18 +506,48 @@ public class Entry {
}
public boolean updateOTP(boolean updateNow) {
- if (type == OTPType.TOTP || type == OTPType.STEAM) {
+ if (type == OTPType.TOTP || type == OTPType.STEAM || type == OTPType.MOTP) {
long time = System.currentTimeMillis() / 1000;
long counter = time / this.getPeriod();
if (updateNow || counter > last_update) {
- if (type == OTPType.TOTP)
- currentOTP = TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm);
- else if (type == OTPType.STEAM)
- currentOTP = TokenCalculator.TOTP_Steam(secret, period, digits, algorithm);
+ // Store the previous token so we don't have to recalculate it every time
+ if (currentOTP != null && !currentOTP.isEmpty())
+ prevOTP = currentOTP;
+ else
+ prevOTP = "";
+
+ switch (type) {
+ case TOTP:
+ currentOTP = TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm, 0);
+
+ if (prevOTP == null || prevOTP.isEmpty())
+ prevOTP = TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm, -1);
+
+ break;
+ case STEAM:
+ currentOTP = TokenCalculator.TOTP_Steam(secret, period, digits, algorithm, 0);
+
+ if (prevOTP == null || prevOTP.isEmpty())
+ prevOTP = TokenCalculator.TOTP_Steam(secret, period, digits, algorithm, -1);
+
+ break;
+ case MOTP:
+ String currentPin = this.getPin();
+
+ if (currentPin.isEmpty()) {
+ currentOTP = MOTP_NO_PIN_CODE;
+ } else {
+ currentOTP = TokenCalculator.MOTP(currentPin, new String(this.secret), time, 0);
+
+ if (prevOTP == null || prevOTP.isEmpty())
+ prevOTP = TokenCalculator.MOTP(currentPin, new String(this.secret), time, -1);
+ }
+
+ break;
+ }
last_update = counter;
- //New OTP. Change color to default color
setColor(COLOR_DEFAULT);
return true;
} else {
@@ -512,22 +556,6 @@ public class Entry {
} else if (type == OTPType.HOTP) {
currentOTP = TokenCalculator.HOTP(secret, counter, digits, algorithm);
return true;
- } else if (type == OTPType.MOTP) {
- long time = System.currentTimeMillis() / 1000;
- long counter = time / this.getPeriod();
- if (counter > last_update || updateNow) {
- String currentPin = this.getPin();
- if (currentPin.isEmpty()) {
- currentOTP = MOTP_NO_PIN_CODE;
- } else {
- currentOTP = TokenCalculator.MOTP(currentPin, new String(this.secret), time);
- }
- last_update = counter;
- setColor(COLOR_DEFAULT);
- return true;
- } else {
- return false;
- }
} else {
return false;
}
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 4a6bcd83..0f41d53e 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
@@ -675,4 +675,8 @@ public class Settings {
String labelDisplay = getString(R.string.settings_key_label_display, R.string.settings_default_label_display);
return Constants.LabelDisplay.valueOf(labelDisplay.toUpperCase(Locale.ENGLISH));
}
+
+ public boolean getShowPrevToken() {
+ return getBoolean(R.string.settings_key_show_prev_token, false);
+ }
}
diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/TokenCalculator.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/TokenCalculator.java
index 7e157c29..96379560 100644
--- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/TokenCalculator.java
+++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/TokenCalculator.java
@@ -61,19 +61,24 @@ public class TokenCalculator {
return mac.doFinal(data);
}
+ // TODO: Rewrite tests so this compatibility wrapper can be removed
public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits, HashAlgorithm algorithm) {
- int fullToken = TOTP(secret, period, time, algorithm);
+ return TOTP_RFC6238(secret, period, time, digits, algorithm, 0);
+ }
+
+ public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits, HashAlgorithm algorithm, int offset) {
+ int fullToken = TOTP(secret, period, time, algorithm, offset);
int div = (int) Math.pow(10, digits);
return fullToken % div;
}
- public static String TOTP_RFC6238(byte[] secret, int period, int digits, HashAlgorithm algorithm) {
- return Tools.formatTokenString(TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits, algorithm), digits);
+ public static String TOTP_RFC6238(byte[] secret, int period, int digits, HashAlgorithm algorithm, int offset) {
+ return Tools.formatTokenString(TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits, algorithm, offset), digits);
}
- public static String TOTP_Steam(byte[] secret, int period, int digits, HashAlgorithm algorithm) {
- int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000, algorithm);
+ public static String TOTP_Steam(byte[] secret, int period, int digits, HashAlgorithm algorithm, int offset) {
+ int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000, algorithm, offset);
StringBuilder tokenBuilder = new StringBuilder();
@@ -92,8 +97,8 @@ public class TokenCalculator {
return Tools.formatTokenString(fullToken % div, digits);
}
- private static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm) {
- return HOTP(key, time / period, algorithm);
+ private static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm, int offset) {
+ return HOTP(key, (time / period) + offset, algorithm);
}
private static int HOTP(byte[] key, long counter, HashAlgorithm algorithm)
@@ -119,9 +124,9 @@ public class TokenCalculator {
return r;
}
- public static String MOTP(String PIN, String secret, long epoch)
+ public static String MOTP(String PIN, String secret, long epoch, int offset)
{
- String epochText = String.valueOf(epoch / 10);
+ String epochText = String.valueOf((epoch / 10) + offset);
String hashText = epochText + secret + PIN;
String otp = "";
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 ef578348..ec2a262f 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
@@ -69,11 +69,11 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
private final LinearLayout coverLayout;
private final LinearLayout counterLayout;
private final FrameLayout thumbnailFrame;
- private final ImageView visibleImg;
private final ImageView thumbnailImg;
private final ImageButton menuButton;
private final ImageButton copyButton;
private final TextView value;
+ private final TextView valuePrev;
private final TextView label;
private final TextView counter;
private final TextView tags;
@@ -86,8 +86,8 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
card = v.findViewById(R.id.card_view);
value = v.findViewById(R.id.valueText);
+ valuePrev = v.findViewById(R.id.valueTextPrev);
valueLayout = v.findViewById(R.id.valueLayout);
- visibleImg = v.findViewById(R.id.valueImg);
thumbnailFrame = v.findViewById(R.id.thumbnailFrame);
thumbnailImg = v.findViewById(R.id.thumbnailImg);
coverLayout = v.findViewById(R.id.coverLayout);
@@ -106,7 +106,6 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
menuButton.getDrawable().setColorFilter(colorFilter);
copyButton.getDrawable().setColorFilter(colorFilter);
- visibleImg.getDrawable().setColorFilter(colorFilter);
invisibleImg.getDrawable().setColorFilter(colorFilter);
setupOnClickListeners(menuButton, copyButton);
@@ -221,6 +220,21 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
// save the unformatted token to the tag of this TextView for copy/paste
value.setTag(entry.getCurrentOTP());
+ if (settings.getShowPrevToken()) {
+ String tokenPrev = entry.getPrevOTP();
+
+ if (tokenPrev != null && !tokenPrev.isEmpty()) {
+ String tokenFormattedPrev = Tools.formatToken(tokenPrev, settings.getTokenSplitGroupSize());
+
+ valuePrev.setVisibility(View.VISIBLE);
+ valuePrev.setText(tokenFormattedPrev);
+ } else {
+ valuePrev.setVisibility(View.GONE);
+ }
+ } else {
+ valuePrev.setVisibility(View.GONE);
+ }
+
List entryTags = entry.getTags();
StringBuilder stringBuilder = new StringBuilder();
@@ -255,11 +269,9 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
if (entry.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);
}
}
}
@@ -316,11 +328,9 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
if (enabled) {
valueLayout.setVisibility(View.GONE);
coverLayout.setVisibility(View.VISIBLE);
- visibleImg.setVisibility(View.VISIBLE);
} else {
valueLayout.setVisibility(View.VISIBLE);
coverLayout.setVisibility(View.GONE);
- visibleImg.setVisibility(View.GONE);
}
}
@@ -369,5 +379,6 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
}
value.setTextColor(textColor);
+ valuePrev.setTextColor(textColor);
}
}
diff --git a/app/src/main/res/layout/component_card_compact.xml b/app/src/main/res/layout/component_card_compact.xml
index f10388b7..40ddb65f 100644
--- a/app/src/main/res/layout/component_card_compact.xml
+++ b/app/src/main/res/layout/component_card_compact.xml
@@ -1,13 +1,14 @@
-
@@ -21,7 +22,8 @@
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:baselineAligned="false">
+ android:baselineAligned="false"
+ tools:ignore="UselessParent">
+ android:src="@mipmap/ic_launcher"
+ tools:ignore="ContentDescription" />
@@ -72,28 +75,31 @@
android:layout_height="wrap_content"
android:visibility="gone">
-
-
+
+
+ android:layout_height="wrap_content"
+ tools:ignore="UseCompoundDrawables">
+ android:textSize="18sp"
+ tools:ignore="HardcodedText" />
+ android:textSize="13.5sp"
+ tools:ignore="HardcodedText" />
@@ -146,12 +155,14 @@
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:visibility="gone"
- android:orientation="horizontal" >
+ android:orientation="horizontal"
+ tools:ignore="UseCompoundDrawables">
+ app:srcCompat="@drawable/ic_alarm_gray"
+ tools:ignore="ContentDescription" />
diff --git a/app/src/main/res/layout/component_card_default.xml b/app/src/main/res/layout/component_card_default.xml
index 9e5f56b0..940598e0 100644
--- a/app/src/main/res/layout/component_card_default.xml
+++ b/app/src/main/res/layout/component_card_default.xml
@@ -2,15 +2,17 @@
+ style="?attr/cardStyle" >
+ android:baselineAligned="false"
+ tools:ignore="UselessParent">
+ android:src="@mipmap/ic_launcher"
+ tools:ignore="ContentDescription" />
@@ -72,28 +76,33 @@
android:layout_height="wrap_content"
android:visibility="gone">
-
-
+ android:textStyle="bold"
+ android:textDirection="ltr"
+ tools:ignore="HardcodedText" />
+
+
+ android:layout_height="wrap_content"
+ android:visibility="visible"
+ tools:ignore="UseCompoundDrawables">
+ android:contentDescription="@string/label_hidden"
+ app:srcCompat="@drawable/ic_visibility_invisible" />
+ android:textSize="18sp"
+ tools:ignore="HardcodedText" />
+ android:textSize="13.5sp"
+ tools:ignore="HardcodedText" />
@@ -146,12 +158,14 @@
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:visibility="gone"
- android:orientation="horizontal" >
+ android:orientation="horizontal"
+ tools:ignore="UseCompoundDrawables">
+ app:srcCompat="@drawable/ic_alarm_gray"
+ tools:ignore="ContentDescription" />
-
@@ -21,7 +22,8 @@
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:baselineAligned="false">
+ android:baselineAligned="false"
+ tools:ignore="UselessParent">
+ android:background="?attr/thumbnailBackground"
+ tools:ignore="RtlSymmetry">
+ android:src="@mipmap/ic_launcher"
+ tools:ignore="ContentDescription" />
@@ -69,13 +73,6 @@
android:layout_height="wrap_content"
android:visibility="gone">
-
-
+
+
+ android:layout_height="wrap_content"
+ tools:ignore="UseCompoundDrawables">
+ android:text="Label"
+ tools:ignore="HardcodedText" />
+ android:text="Tags"
+ tools:ignore="HardcodedText" />
@@ -143,12 +153,14 @@
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:visibility="gone"
- android:orientation="horizontal" >
+ android:orientation="horizontal"
+ tools:ignore="UseCompoundDrawables">
+ app:srcCompat="@drawable/ic_alarm_gray"
+ tools:ignore="ContentDescription" />
diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml
index 30518aa7..0170db29 100644
--- a/app/src/main/res/values/settings.xml
+++ b/app/src/main/res/values/settings.xml
@@ -29,6 +29,7 @@
perf_theme_mode
pref_theme_black_auto
pref_theme
+ pref_show_prev_token
pref_label_size_sp
pref_card_layout
pref_label_scroll
diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml
index 61d71ea7..44767cc2 100644
--- a/app/src/main/res/values/strings_settings.xml
+++ b/app/src/main/res/values/strings_settings.xml
@@ -1,5 +1,8 @@
-
+
+
Settings
@@ -28,6 +31,7 @@
Black theme
Theme
Card layout
+ Show previous token
Label font size
Label display
Single-tap
@@ -84,6 +88,7 @@
above 8.0 (Oreo)
Use the black theme in dark mode
+ Show the previous token in addition to the current one
App will be minimized when you copy the OTP to clipboard
Specify which values should be included when searching
Highlights token in red if it\'s expiring in 8 seconds
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 4f3f7069..742cf8c6 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -143,6 +143,12 @@
android:entryValues="@array/settings_values_tap"
android:defaultValue="@string/settings_default_tap_double" />
+
+