HOTP support
This adds the initial (already working) draft of the HOTP implementation Closes #5
This commit is contained in:
parent
3294b62385
commit
836cef6265
6 changed files with 224 additions and 35 deletions
|
@ -42,15 +42,17 @@ import java.util.Set;
|
|||
|
||||
public class Entry {
|
||||
public enum OTPType {
|
||||
TOTP, STEAM
|
||||
TOTP, HOTP, STEAM
|
||||
}
|
||||
public static Set<OTPType> PublicTypes = EnumSet.of(OTPType.TOTP);
|
||||
public static Set<OTPType> PublicTypes = EnumSet.of(OTPType.TOTP, OTPType.HOTP);
|
||||
|
||||
private static final OTPType DEFAULT_TYPE = OTPType.TOTP;
|
||||
private static final int DEFAULT_PERIOD = 30;
|
||||
|
||||
private static final String JSON_SECRET = "secret";
|
||||
private static final String JSON_LABEL = "label";
|
||||
private static final String JSON_PERIOD = "period";
|
||||
private static final String JSON_COUNTER = "counter";
|
||||
private static final String JSON_DIGITS = "digits";
|
||||
private static final String JSON_TYPE = "type";
|
||||
private static final String JSON_ALGORITHM = "algorithm";
|
||||
|
@ -63,6 +65,7 @@ public class Entry {
|
|||
private int digits = TokenCalculator.TOTP_DEFAULT_DIGITS;
|
||||
private TokenCalculator.HashAlgorithm algorithm = TokenCalculator.DEFAULT_ALGORITHM;
|
||||
private byte[] secret;
|
||||
private long counter;
|
||||
private String label;
|
||||
private String currentOTP;
|
||||
private boolean visible = false;
|
||||
|
@ -84,6 +87,16 @@ public class Entry {
|
|||
this.tags = tags;
|
||||
}
|
||||
|
||||
public Entry(OTPType type, String secret, long counter, int digits, String label, TokenCalculator.HashAlgorithm algorithm, List<String> tags) {
|
||||
this.type = type;
|
||||
this.secret = new Base32().decode(secret.toUpperCase());
|
||||
this.counter = counter;
|
||||
this.digits = digits;
|
||||
this.label = label;
|
||||
this.algorithm = algorithm;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public Entry(String contents) throws Exception {
|
||||
contents = contents.replaceFirst("otpauth", "http");
|
||||
Uri uri = Uri.parse(contents);
|
||||
|
@ -95,23 +108,32 @@ public class Entry {
|
|||
|
||||
if(url.getHost().equals("totp")){
|
||||
type = OTPType.TOTP;
|
||||
} else if (url.getHost().equals("hotp")) {
|
||||
type = OTPType.HOTP;
|
||||
} else {
|
||||
throw new Exception();
|
||||
throw new Exception("unknown otp type");
|
||||
}
|
||||
|
||||
String secret = uri.getQueryParameter("secret");
|
||||
String label = uri.getPath().substring(1);
|
||||
|
||||
String counter = uri.getQueryParameter("counter");
|
||||
String issuer = uri.getQueryParameter("issuer");
|
||||
String period = uri.getQueryParameter("period");
|
||||
String digits = uri.getQueryParameter("digits");
|
||||
String algorithm = uri.getQueryParameter("algorithm");
|
||||
List<String> tags = uri.getQueryParameters("tags");
|
||||
|
||||
if(issuer != null){
|
||||
if (issuer != null){
|
||||
label = issuer +" - "+label;
|
||||
}
|
||||
|
||||
if (type == OTPType.HOTP && counter == null) {
|
||||
throw new Exception("missing counter for HOTP");
|
||||
} else {
|
||||
this.counter = Long.parseLong(counter);
|
||||
}
|
||||
|
||||
this.label = label;
|
||||
this.secret = new Base32().decode(secret.toUpperCase());
|
||||
|
||||
|
@ -140,16 +162,10 @@ public class Entry {
|
|||
}
|
||||
}
|
||||
|
||||
public Entry (JSONObject jsonObj) throws JSONException {
|
||||
public Entry (JSONObject jsonObj)
|
||||
throws Exception {
|
||||
this.secret = new Base32().decode(jsonObj.getString(JSON_SECRET).toUpperCase());
|
||||
this.label = jsonObj.getString(JSON_LABEL);
|
||||
this.period = jsonObj.getInt(JSON_PERIOD);
|
||||
|
||||
try {
|
||||
this.digits = jsonObj.getInt(JSON_DIGITS);
|
||||
} catch(Exception e) {
|
||||
this.digits = TokenCalculator.TOTP_DEFAULT_DIGITS;
|
||||
}
|
||||
|
||||
try {
|
||||
this.type = OTPType.valueOf(jsonObj.getString(JSON_TYPE));
|
||||
|
@ -157,6 +173,26 @@ public class Entry {
|
|||
this.type = DEFAULT_TYPE;
|
||||
}
|
||||
|
||||
try {
|
||||
this.period = jsonObj.getInt(JSON_PERIOD);
|
||||
} catch(Exception e) {
|
||||
if (type == OTPType.TOTP)
|
||||
this.period = DEFAULT_PERIOD;
|
||||
}
|
||||
|
||||
try {
|
||||
this.counter = jsonObj.getLong(JSON_COUNTER);
|
||||
} catch (Exception e) {
|
||||
if (type == OTPType.HOTP)
|
||||
throw new Exception("missing counter for HOTP");
|
||||
}
|
||||
|
||||
try {
|
||||
this.digits = jsonObj.getInt(JSON_DIGITS);
|
||||
} catch(Exception e) {
|
||||
this.digits = TokenCalculator.TOTP_DEFAULT_DIGITS;
|
||||
}
|
||||
|
||||
try {
|
||||
this.algorithm = TokenCalculator.HashAlgorithm.valueOf(jsonObj.getString(JSON_ALGORITHM));
|
||||
} catch (Exception e) {
|
||||
|
@ -190,13 +226,17 @@ public class Entry {
|
|||
JSONObject jsonObj = new JSONObject();
|
||||
jsonObj.put(JSON_SECRET, new String(new Base32().encode(getSecret())));
|
||||
jsonObj.put(JSON_LABEL, getLabel());
|
||||
jsonObj.put(JSON_PERIOD, getPeriod());
|
||||
jsonObj.put(JSON_DIGITS, getDigits());
|
||||
jsonObj.put(JSON_TYPE, getType().toString());
|
||||
jsonObj.put(JSON_ALGORITHM, algorithm.toString());
|
||||
jsonObj.put(JSON_THUMBNAIL, getThumbnail().name());
|
||||
jsonObj.put(JSON_LAST_USED, getLastUsed());
|
||||
|
||||
if (type == OTPType.TOTP)
|
||||
jsonObj.put(JSON_PERIOD, getPeriod());
|
||||
else if (type == OTPType.HOTP)
|
||||
jsonObj.put(JSON_COUNTER, getCounter());
|
||||
|
||||
JSONArray tagsArray = new JSONArray();
|
||||
for(String tag : tags){
|
||||
tagsArray.put(tag);
|
||||
|
@ -238,6 +278,14 @@ public class Entry {
|
|||
this.period = period;
|
||||
}
|
||||
|
||||
public long getCounter() {
|
||||
return counter;
|
||||
}
|
||||
|
||||
public void setCounter(long counter) {
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
public int getDigits() {
|
||||
return digits;
|
||||
}
|
||||
|
@ -311,6 +359,9 @@ public class Entry {
|
|||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (type == OTPType.HOTP) {
|
||||
currentOTP = TokenCalculator.HOTP(secret, counter, digits, algorithm);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -324,6 +375,7 @@ public class Entry {
|
|||
return period == entry.period &&
|
||||
digits == entry.digits &&
|
||||
type == entry.type &&
|
||||
counter == entry.counter &&
|
||||
algorithm == entry.algorithm &&
|
||||
Arrays.equals(secret, entry.secret) &&
|
||||
Objects.equals(label, entry.label);
|
||||
|
@ -331,6 +383,6 @@ public class Entry {
|
|||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, period, digits, algorithm, secret, label);
|
||||
return Objects.hash(type, period, counter, digits, algorithm, secret, label);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ import java.security.NoSuchAlgorithmException;
|
|||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
|
||||
public class TokenCalculator {
|
||||
public static final int TOTP_DEFAULT_PERIOD = 30;
|
||||
public static final int TOTP_DEFAULT_DIGITS = 6;
|
||||
|
@ -84,14 +83,23 @@ public class TokenCalculator {
|
|||
return tokenBuilder.toString();
|
||||
}
|
||||
|
||||
public static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm)
|
||||
public static String HOTP(byte[] secret, long counter, int digits, HashAlgorithm algorithm) {
|
||||
int fullToken = HOTP(secret, counter, algorithm);
|
||||
int div = (int) Math.pow(10, digits);
|
||||
|
||||
return String.format("%0" + digits + "d", fullToken % div);
|
||||
}
|
||||
|
||||
private static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm) {
|
||||
return HOTP(key, time / period, algorithm);
|
||||
}
|
||||
|
||||
private static int HOTP(byte[] key, long counter, HashAlgorithm algorithm)
|
||||
{
|
||||
int r = 0;
|
||||
|
||||
try {
|
||||
long timeInterval = time / period;
|
||||
|
||||
byte[] data = ByteBuffer.allocate(8).putLong(timeInterval).array();
|
||||
byte[] data = ByteBuffer.allocate(8).putLong(counter).array();
|
||||
byte[] hash = generateHash(algorithm, key, data);
|
||||
|
||||
int offset = hash[hash.length - 1] & 0xF;
|
||||
|
|
|
@ -28,9 +28,11 @@ import android.content.ClipboardManager;
|
|||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.PopupMenu;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -62,6 +64,7 @@ import java.util.Comparator;
|
|||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
@ -122,7 +125,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
return entries.indexOf(displayedEntries.get(displayPosition));
|
||||
}
|
||||
|
||||
public void entriesChanged() {
|
||||
private void entriesChanged() {
|
||||
displayedEntries = sortEntries(entries);
|
||||
filterByTags(tagsFilter);
|
||||
notifyDataSetChanged();
|
||||
|
@ -181,15 +184,16 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(EntryViewHolder entryViewHolder, int i) {
|
||||
public void onBindViewHolder(@NonNull EntryViewHolder entryViewHolder, int i) {
|
||||
Entry entry = displayedEntries.get(i);
|
||||
|
||||
entryViewHolder.updateValues(entry.getLabel(), entry.getCurrentOTP(), entry.getTags(), entry.getThumbnail(), entry.isVisible());
|
||||
entryViewHolder.updateValues(entry);
|
||||
|
||||
if (entry.hasNonDefaultPeriod()) {
|
||||
entryViewHolder.showCustomPeriod(entry.getPeriod());
|
||||
} else {
|
||||
entryViewHolder.hideCustomPeriod();
|
||||
if (entry.getType() == Entry.OTPType.TOTP || entry.getType() == Entry.OTPType.STEAM) {
|
||||
if (entry.hasNonDefaultPeriod())
|
||||
entryViewHolder.showCustomPeriod(entry.getPeriod());
|
||||
else
|
||||
entryViewHolder.hideCustomPeriod();
|
||||
}
|
||||
|
||||
entryViewHolder.setLabelSize(settings.getLabelSize());
|
||||
|
@ -199,8 +203,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
entryViewHolder.setLabelScroll(settings.getScrollLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntryViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
|
||||
@Override @NonNull
|
||||
public EntryViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.component_card, viewGroup, false);
|
||||
|
||||
EntryViewHolder viewHolder = new EntryViewHolder(context, itemView, settings.getTapToReveal());
|
||||
|
@ -250,6 +254,27 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCounterTapped(int position) {
|
||||
Entry entry = displayedEntries.get(position);
|
||||
Entry realEntry = entries.get(getRealIndex(position));
|
||||
|
||||
long counter = entry.getCounter() + 1;
|
||||
|
||||
entry.setCounter(counter);
|
||||
entry.updateOTP();
|
||||
notifyItemChanged(position);
|
||||
|
||||
realEntry.setCounter(counter);
|
||||
realEntry.updateOTP();
|
||||
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCounterLongPressed(int position) {
|
||||
setCounter(position);
|
||||
}
|
||||
});
|
||||
|
||||
return viewHolder;
|
||||
|
@ -275,6 +300,47 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
}
|
||||
}
|
||||
|
||||
private void setCounter(final int pos) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
|
||||
int marginSmall = context.getResources().getDimensionPixelSize(R.dimen.activity_margin_small);
|
||||
int marginMedium = context.getResources().getDimensionPixelSize(R.dimen.activity_margin_medium);
|
||||
|
||||
final EditText input = new EditText(context);
|
||||
input.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
input.setText(String.format(Locale.ENGLISH, "%d", displayedEntries.get(pos).getCounter()));
|
||||
input.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
input.setSingleLine();
|
||||
|
||||
FrameLayout container = new FrameLayout(context);
|
||||
container.setPaddingRelative(marginMedium, marginSmall, marginMedium, 0);
|
||||
container.addView(input);
|
||||
|
||||
builder.setTitle(R.string.dialog_title_counter)
|
||||
.setView(container)
|
||||
.setPositiveButton(R.string.button_save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
int realIndex = getRealIndex(pos);
|
||||
long newCounter = Long.parseLong(input.getEditableText().toString());
|
||||
|
||||
displayedEntries.get(pos).setCounter(newCounter);
|
||||
notifyItemChanged(pos);
|
||||
|
||||
Entry e = entries.get(realIndex);
|
||||
e.setCounter(newCounter);
|
||||
|
||||
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private boolean updateLastUsed(int position, int realIndex) {
|
||||
long timeStamp = System.currentTimeMillis();
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import android.widget.ImageView;
|
|||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.shadowice.flocke.andotp.Database.Entry;
|
||||
import org.shadowice.flocke.andotp.R;
|
||||
import org.shadowice.flocke.andotp.Utilities.EntryThumbnail;
|
||||
import org.shadowice.flocke.andotp.Utilities.Settings;
|
||||
|
@ -41,6 +42,7 @@ import org.shadowice.flocke.andotp.Utilities.Tools;
|
|||
import org.shadowice.flocke.andotp.View.ItemTouchHelper.ItemTouchHelperViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class EntryViewHolder extends RecyclerView.ViewHolder
|
||||
implements ItemTouchHelperViewHolder {
|
||||
|
@ -52,11 +54,13 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
|||
private LinearLayout valueLayout;
|
||||
private LinearLayout coverLayout;
|
||||
private LinearLayout customPeriodLayout;
|
||||
private LinearLayout counterLayout;
|
||||
private FrameLayout thumbnailFrame;
|
||||
private ImageView visibleImg;
|
||||
private ImageView thumbnailImg;
|
||||
private TextView value;
|
||||
private TextView label;
|
||||
private TextView counter;
|
||||
private TextView tags;
|
||||
private TextView customPeriod;
|
||||
|
||||
|
@ -77,6 +81,8 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
|||
tags = v.findViewById(R.id.textViewTags);
|
||||
customPeriodLayout = v.findViewById(R.id.customPeriodLayout);
|
||||
customPeriod = v.findViewById(R.id.customPeriod);
|
||||
counterLayout = v.findViewById(R.id.counterLayout);
|
||||
counter = v.findViewById(R.id.counter);
|
||||
|
||||
ImageButton menuButton = v.findViewById(R.id.menuButton);
|
||||
ImageButton copyButton = v.findViewById(R.id.copyButton);
|
||||
|
@ -107,17 +113,45 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
|||
}
|
||||
});
|
||||
|
||||
counterLayout.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (callback != null)
|
||||
callback.onCounterTapped(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
counterLayout.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (callback != null)
|
||||
callback.onCounterLongPressed(getAdapterPosition());
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
setTapToReveal(tapToReveal);
|
||||
}
|
||||
|
||||
public void updateValues(String label, String token, List<String> tags, EntryThumbnail.EntryThumbnails thumbnail, boolean isVisible) {
|
||||
public void updateValues(Entry entry) {
|
||||
Settings settings = new Settings(context);
|
||||
final String tokenFormatted = Tools.formatToken(token, settings.getTokenSplitGroupSize());
|
||||
|
||||
this.label.setText(label);
|
||||
if (entry.getType() == Entry.OTPType.HOTP) {
|
||||
counterLayout.setVisibility(View.VISIBLE);
|
||||
counter.setText(String.format(Locale.ENGLISH, "%d", entry.getCounter()));
|
||||
} else {
|
||||
counterLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final String tokenFormatted = Tools.formatToken(entry.getCurrentOTP(), settings.getTokenSplitGroupSize());
|
||||
|
||||
this.label.setText(entry.getLabel());
|
||||
value.setText(tokenFormatted);
|
||||
// save the unformatted token to the tag of this TextView for copy/paste
|
||||
value.setTag(token);
|
||||
value.setTag(entry.getCurrentOTP());
|
||||
|
||||
List<String> tags = entry.getTags();
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for(int i = 0; i < tags.size(); i++) {
|
||||
|
@ -138,11 +172,11 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
|||
|
||||
int thumbnailSize = settings.getThumbnailSize();
|
||||
if(settings.getThumbnailVisible()) {
|
||||
thumbnailImg.setImageBitmap(EntryThumbnail.getThumbnailGraphic(context, label, thumbnailSize, thumbnail));
|
||||
thumbnailImg.setImageBitmap(EntryThumbnail.getThumbnailGraphic(context, entry.getLabel(), thumbnailSize, entry.getThumbnail()));
|
||||
}
|
||||
|
||||
if (this.tapToReveal) {
|
||||
if (isVisible) {
|
||||
if (entry.isVisible()) {
|
||||
valueLayout.setVisibility(View.VISIBLE);
|
||||
coverLayout.setVisibility(View.GONE);
|
||||
visibleImg.setVisibility(View.GONE);
|
||||
|
@ -156,7 +190,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
|||
|
||||
public void showCustomPeriod(int period) {
|
||||
customPeriodLayout.setVisibility(View.VISIBLE);
|
||||
customPeriod.setText(String.format(context.getString(R.string.format_custom_period), period));
|
||||
customPeriod.setText(String.format(Locale.ENGLISH, context.getString(R.string.format_custom_period), period));
|
||||
}
|
||||
|
||||
public void hideCustomPeriod() {
|
||||
|
@ -230,5 +264,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
|||
void onMenuButtonClicked(View parentView, int position);
|
||||
void onCopyButtonClicked(String text, int position);
|
||||
void onTap(int position);
|
||||
void onCounterTapped(int position);
|
||||
void onCounterLongPressed(int position);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,10 +140,34 @@
|
|||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/counterLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin"
|
||||
android:gravity="center"
|
||||
android:visibility="visible"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/label_counter_symbol" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/counter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="@dimen/activity_margin_small"
|
||||
android:src="@drawable/ic_content_copy_gray"
|
||||
android:background="?android:attr/selectableItemBackground"/>
|
||||
|
@ -152,6 +176,7 @@
|
|||
android:id="@+id/menuButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="@dimen/activity_margin_small"
|
||||
android:src="@drawable/ic_more_vert_gray"
|
||||
android:background="?android:attr/selectableItemBackground"/>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<string name="label_algorithm">Algorithm</string>
|
||||
<string name="label_tags">Tags</string>
|
||||
<string name="label_advanced">Advanced options</string>
|
||||
<string name="label_counter_symbol" translatable="false">#</string>
|
||||
|
||||
<!-- Drawer -->
|
||||
<string name="drawer_open">Show tags</string>
|
||||
|
@ -63,6 +64,7 @@
|
|||
<string name="dialog_title_manual_entry">Enter details</string>
|
||||
<string name="dialog_title_remove">Remove</string>
|
||||
<string name="dialog_title_rename">Rename</string>
|
||||
<string name="dialog_title_counter">Counter</string>
|
||||
<string name="dialog_title_last_used">Last used</string>
|
||||
<string name="dialog_title_keystore_error">KeyStore error</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue