Merge pull request #617 from mantinan/master
This implements MOTP support
This commit is contained in:
commit
cca3972b6d
10 changed files with 218 additions and 17 deletions
|
@ -26,6 +26,7 @@ package org.shadowice.flocke.andotp.Database;
|
|||
import android.net.Uri;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -40,11 +41,12 @@ import java.util.Objects;
|
|||
|
||||
public class Entry {
|
||||
public enum OTPType {
|
||||
TOTP, HOTP, STEAM
|
||||
TOTP, HOTP, MOTP, STEAM
|
||||
}
|
||||
|
||||
private static final OTPType DEFAULT_TYPE = OTPType.TOTP;
|
||||
private static final int DEFAULT_PERIOD = 30;
|
||||
private static final String MOTP_NO_PIN_CODE = "PINREQ";
|
||||
|
||||
private static final String JSON_SECRET = "secret";
|
||||
private static final String JSON_ISSUER = "issuer";
|
||||
|
@ -79,6 +81,7 @@ public class Entry {
|
|||
public static final int COLOR_RED = 1;
|
||||
private static final int EXPIRY_TIME = 8;
|
||||
private int color = COLOR_DEFAULT;
|
||||
private String pin = "";
|
||||
private long listId = 0;
|
||||
|
||||
public Entry(){}
|
||||
|
@ -107,6 +110,16 @@ public class Entry {
|
|||
setThumbnailFromIssuer(issuer);
|
||||
}
|
||||
|
||||
public Entry(OTPType type, String secret, String issuer, String label, List<String> tags) {
|
||||
this.type = type;
|
||||
this.secret = secret.getBytes();
|
||||
this.issuer = issuer;
|
||||
this.label = label;
|
||||
this.tags = tags;
|
||||
this.period = TokenCalculator.TOTP_DEFAULT_PERIOD;
|
||||
setThumbnailFromIssuer(issuer);
|
||||
}
|
||||
|
||||
public Entry(String contents) throws Exception {
|
||||
contents = contents.replaceFirst("otpauth", "http");
|
||||
Uri uri = Uri.parse(contents);
|
||||
|
@ -123,6 +136,9 @@ public class Entry {
|
|||
case "hotp":
|
||||
type = OTPType.HOTP;
|
||||
break;
|
||||
case "motp":
|
||||
type = OTPType.MOTP;
|
||||
break;
|
||||
case "steam":
|
||||
type = OTPType.STEAM;
|
||||
break;
|
||||
|
@ -156,7 +172,11 @@ public class Entry {
|
|||
|
||||
this.issuer = issuer;
|
||||
this.label = label;
|
||||
this.secret = new Base32().decode(secret.toUpperCase());
|
||||
if(type == OTPType.MOTP) {
|
||||
this.secret = secret.getBytes();
|
||||
} else {
|
||||
this.secret = new Base32().decode(secret.toUpperCase());
|
||||
}
|
||||
|
||||
if (digits != null) {
|
||||
this.digits = Integer.parseInt(digits);
|
||||
|
@ -292,14 +312,22 @@ public class Entry {
|
|||
case STEAM:
|
||||
type = "steam";
|
||||
break;
|
||||
case MOTP:
|
||||
type = "motp";
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
Uri.Builder builder = new Uri.Builder()
|
||||
.scheme("otpauth")
|
||||
.authority(type)
|
||||
.appendPath(this.label)
|
||||
.appendQueryParameter("secret", new Base32().encodeAsString(this.secret));
|
||||
.appendPath(this.label);
|
||||
|
||||
if (this.type == OTPType.MOTP)
|
||||
builder.appendQueryParameter("secret", new String(this.secret));
|
||||
else
|
||||
builder.appendQueryParameter("secret", new Base32().encodeAsString(this.secret));
|
||||
|
||||
if (this.issuer != null) {
|
||||
builder.appendQueryParameter("issuer", this.issuer);
|
||||
}
|
||||
|
@ -324,7 +352,7 @@ public class Entry {
|
|||
}
|
||||
|
||||
public boolean isTimeBased() {
|
||||
return type == OTPType.TOTP || type == OTPType.STEAM;
|
||||
return type == OTPType.TOTP || type == OTPType.STEAM || type == OTPType.MOTP;
|
||||
}
|
||||
|
||||
public boolean isCounterBased() { return type == OTPType.HOTP; }
|
||||
|
@ -342,7 +370,10 @@ public class Entry {
|
|||
}
|
||||
|
||||
public String getSecretEncoded() {
|
||||
return new String(new Base32().encode(secret));
|
||||
if (type == OTPType.MOTP)
|
||||
return new String(secret);
|
||||
else
|
||||
return new String(new Base32().encode(secret));
|
||||
}
|
||||
|
||||
public void setSecret(byte[] secret) {
|
||||
|
@ -444,6 +475,14 @@ public class Entry {
|
|||
return currentOTP;
|
||||
}
|
||||
|
||||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public void setPin(String pin) {
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
public long getListId() {
|
||||
return listId;
|
||||
}
|
||||
|
@ -452,12 +491,12 @@ public class Entry {
|
|||
listId = newId;
|
||||
}
|
||||
|
||||
public boolean updateOTP(boolean force) {
|
||||
public boolean updateOTP(boolean updateNow) {
|
||||
if (type == OTPType.TOTP || type == OTPType.STEAM) {
|
||||
long time = System.currentTimeMillis() / 1000;
|
||||
long counter = time / this.getPeriod();
|
||||
|
||||
if (force || counter > last_update) {
|
||||
if (updateNow || counter > last_update) {
|
||||
if (type == OTPType.TOTP)
|
||||
currentOTP = TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm);
|
||||
else if (type == OTPType.STEAM)
|
||||
|
@ -473,6 +512,22 @@ 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;
|
||||
}
|
||||
|
@ -482,7 +537,7 @@ public class Entry {
|
|||
* Checks if the OTP is expiring. The color for the entry will be changed to red if the expiry time is less than or equal to 8 seconds
|
||||
* COLOR_DEFAULT indicates that the OTP has not expired. In this case check if the OTP is about to expire. Update color to COLOR_RED if it's about to expire
|
||||
* COLOR_RED indicates that the OTP is already about to expire. Don't check again.
|
||||
* The color will be reset to COLOR_DEFAULT in {@link #updateOTP(boolean force)} method
|
||||
* The color will be reset to COLOR_DEFAULT in {@link #updateOTP(boolean updateNow)} method
|
||||
*
|
||||
* @return Return true only if the color has changed to red to save from unnecessary notifying dataset
|
||||
* */
|
||||
|
@ -542,9 +597,12 @@ public class Entry {
|
|||
return color;
|
||||
}
|
||||
|
||||
public static boolean validateSecret(String secret) {
|
||||
public static boolean validateSecret(String secret, OTPType type) {
|
||||
try {
|
||||
new Base32().decode(secret.toUpperCase());
|
||||
if (type == OTPType.MOTP)
|
||||
Hex.decodeHex(secret);
|
||||
else
|
||||
new Base32().decode(secret.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ public class ManualEntryDialog {
|
|||
final LinearLayout periodLayout = inputView.findViewById(R.id.manual_layout_period);
|
||||
final Spinner algorithmInput = inputView.findViewById(R.id.manual_algorithm);
|
||||
final Button tagsInput = inputView.findViewById(R.id.manual_tags);
|
||||
final Button expandButton = inputView.findViewById(R.id.dialog_expand_button);
|
||||
|
||||
final ArrayAdapter<TokenCalculator.HashAlgorithm> algorithmAdapter = new ArrayAdapter<>(callingActivity, android.R.layout.simple_expandable_list_item_1, TokenCalculator.HashAlgorithm.values());
|
||||
final ArrayAdapter<Entry.OTPType> typeAdapter = new ArrayAdapter<>(callingActivity, android.R.layout.simple_expandable_list_item_1, Entry.OTPType.values());
|
||||
|
@ -101,6 +102,7 @@ public class ManualEntryDialog {
|
|||
if (type == Entry.OTPType.STEAM) {
|
||||
counterLayout.setVisibility(View.GONE);
|
||||
periodLayout.setVisibility(View.VISIBLE);
|
||||
expandButton.setVisibility(View.VISIBLE);
|
||||
|
||||
digitsInput.setText(String.format(Locale.US, "%d", TokenCalculator.STEAM_DEFAULT_DIGITS));
|
||||
periodInput.setText(String.format(Locale.US, "%d", TokenCalculator.TOTP_DEFAULT_PERIOD));
|
||||
|
@ -112,6 +114,7 @@ public class ManualEntryDialog {
|
|||
} else if (type == Entry.OTPType.TOTP) {
|
||||
counterLayout.setVisibility(View.GONE);
|
||||
periodLayout.setVisibility(View.VISIBLE);
|
||||
expandButton.setVisibility(View.VISIBLE);
|
||||
|
||||
if (isNewEntry)
|
||||
digitsInput.setText(String.format(Locale.US, "%d", TokenCalculator.TOTP_DEFAULT_DIGITS));
|
||||
|
@ -120,11 +123,19 @@ public class ManualEntryDialog {
|
|||
} else if (type == Entry.OTPType.HOTP) {
|
||||
counterLayout.setVisibility(View.VISIBLE);
|
||||
periodLayout.setVisibility(View.GONE);
|
||||
expandButton.setVisibility(View.VISIBLE);
|
||||
|
||||
if (isNewEntry)
|
||||
digitsInput.setText(String.format(Locale.US, "%d", TokenCalculator.TOTP_DEFAULT_DIGITS));
|
||||
|
||||
algorithmInput.setEnabled(isNewEntry);
|
||||
}else if (type == Entry.OTPType.MOTP) {
|
||||
counterLayout.setVisibility(View.GONE);
|
||||
periodLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
digitsInput.setText(String.format(Locale.US, "%d", TokenCalculator.TOTP_DEFAULT_DIGITS));
|
||||
expandButton.setVisibility(View.GONE);
|
||||
algorithmInput.setEnabled(isNewEntry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,8 +167,6 @@ public class ManualEntryDialog {
|
|||
|
||||
tagsInput.setOnClickListener(view -> TagsDialog.show(callingActivity, tagsAdapter, tagsCallable, tagsCallable));
|
||||
|
||||
final Button expandButton = inputView.findViewById(R.id.dialog_expand_button);
|
||||
|
||||
// Dirty fix for the compound drawable to avoid crashes on KitKat
|
||||
expandButton.setCompoundDrawablesWithIntrinsicBounds(null, null, ResourcesCompat.getDrawable(callingActivity.getResources(), R.drawable.ic_arrow_down_accent, null), null);
|
||||
|
||||
|
@ -193,13 +202,13 @@ public class ManualEntryDialog {
|
|||
positiveButton.setOnClickListener(view -> {
|
||||
//Replace spaces with empty characters
|
||||
String secret = secretInput.getText().toString().replaceAll("\\s+","");
|
||||
Entry.OTPType type = (Entry.OTPType) typeInput.getSelectedItem();
|
||||
|
||||
if (!Entry.validateSecret(secret)) {
|
||||
if (!Entry.validateSecret(secret, type)) {
|
||||
secretInput.setError(callingActivity.getString(R.string.error_invalid_secret));
|
||||
return;
|
||||
}
|
||||
|
||||
Entry.OTPType type = (Entry.OTPType) typeInput.getSelectedItem();
|
||||
TokenCalculator.HashAlgorithm algorithm = (TokenCalculator.HashAlgorithm) algorithmInput.getSelectedItem();
|
||||
int digits = Integer.parseInt(digitsInput.getText().toString());
|
||||
|
||||
|
@ -250,6 +259,27 @@ public class ManualEntryDialog {
|
|||
if (updateCallback != null)
|
||||
updateCallback.onUpdate();
|
||||
}
|
||||
} else if (type == Entry.OTPType.MOTP) {
|
||||
if (isNewEntry) {
|
||||
Entry newEntry;
|
||||
|
||||
newEntry = new Entry(type, secret, issuer, label, tagsAdapter.getActiveTags());
|
||||
newEntry.updateOTP(false);
|
||||
newEntry.setLastUsed(System.currentTimeMillis());
|
||||
|
||||
adapter.addEntry(newEntry);
|
||||
} else {
|
||||
oldEntry.setIssuer(issuer, true);
|
||||
oldEntry.setLabel(label);
|
||||
oldEntry.setTags(tagsAdapter.getActiveTags());
|
||||
|
||||
oldEntry.updateOTP(false);
|
||||
|
||||
if (updateCallback != null)
|
||||
updateCallback.onUpdate();
|
||||
}
|
||||
|
||||
callingActivity.refreshTags();
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
|
|
|
@ -23,8 +23,11 @@
|
|||
|
||||
package org.shadowice.flocke.andotp.Utilities;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
@ -115,4 +118,26 @@ public class TokenCalculator {
|
|||
|
||||
return r;
|
||||
}
|
||||
|
||||
public static String MOTP(String PIN, String secret, long epoch)
|
||||
{
|
||||
String epochText = String.valueOf(epoch / 10);
|
||||
String hashText = epochText + secret + PIN;
|
||||
String otp = "";
|
||||
|
||||
try {
|
||||
// Create MD5 Hash
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(hashText.getBytes());
|
||||
byte[] messageDigest = digest.digest();
|
||||
|
||||
// Create Hex String
|
||||
String hexString = Hex.encodeHexString(messageDigest);
|
||||
otp = hexString.substring(0, 6);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return otp;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,10 +48,17 @@ public class UIHelper {
|
|||
.show();
|
||||
}
|
||||
|
||||
public static void showKeyboard(Context context, View view) {
|
||||
public static void showKeyboard(Context context, View view){
|
||||
showKeyboard(context,view,false);
|
||||
}
|
||||
|
||||
public static void showKeyboard(Context context, View view, Boolean showForced) {
|
||||
if (view != null) {
|
||||
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(view, 0);
|
||||
if(showForced)
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
||||
else
|
||||
imm.showSoftInput(view, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,8 +37,10 @@ import android.text.Editable;
|
|||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
@ -66,6 +68,7 @@ import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
|
|||
import org.shadowice.flocke.andotp.Utilities.EntryThumbnail;
|
||||
import org.shadowice.flocke.andotp.Utilities.Settings;
|
||||
import org.shadowice.flocke.andotp.Utilities.Tools;
|
||||
import org.shadowice.flocke.andotp.Utilities.UIHelper;
|
||||
import org.shadowice.flocke.andotp.View.ItemTouchHelper.ItemTouchHelperAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -93,6 +96,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
private final TagsAdapter tagsFilterAdapter;
|
||||
private final Settings settings;
|
||||
|
||||
private static final int ESTABLISH_PIN_MENU_INDEX = 4;
|
||||
|
||||
public EntriesCardAdapter(Context context, TagsAdapter tagsFilterAdapter) {
|
||||
this.context = context;
|
||||
this.tagsFilterAdapter = tagsFilterAdapter;
|
||||
|
@ -306,15 +311,21 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
public void onCardSingleClicked(final int position, final String text) {
|
||||
switch (settings.getTapSingle()) {
|
||||
case REVEAL:
|
||||
establishPinIfNeeded(position);
|
||||
cardTapToRevealHandler(position);
|
||||
break;
|
||||
case COPY:
|
||||
establishPinIfNeeded(position);
|
||||
copyHandler(position, text, false);
|
||||
break;
|
||||
case COPY_BACKGROUND:
|
||||
establishPinIfNeeded(position);
|
||||
copyHandler(position, text, true);
|
||||
break;
|
||||
default:
|
||||
// If tap-to-reveal is disabled a single tab still needs to establish the PIN
|
||||
if (!settings.getTapToReveal())
|
||||
establishPinIfNeeded(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -323,12 +334,15 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
public void onCardDoubleClicked(final int position, final String text) {
|
||||
switch (settings.getTapDouble()) {
|
||||
case REVEAL:
|
||||
establishPinIfNeeded(position);
|
||||
cardTapToRevealHandler(position);
|
||||
break;
|
||||
case COPY:
|
||||
establishPinIfNeeded(position);
|
||||
copyHandler(position, text, false);
|
||||
break;
|
||||
case COPY_BACKGROUND:
|
||||
establishPinIfNeeded(position);
|
||||
copyHandler(position, text, true);
|
||||
break;
|
||||
default:
|
||||
|
@ -350,6 +364,13 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
return viewHolder;
|
||||
}
|
||||
|
||||
private void establishPinIfNeeded(int position) {
|
||||
final Entry entry = displayedEntries.get(position);
|
||||
|
||||
if (entry.getType() == Entry.OTPType.MOTP && entry.getPin().isEmpty())
|
||||
establishPIN(position);
|
||||
}
|
||||
|
||||
private void copyHandler(final int position, final String text, final boolean dropToBackground) {
|
||||
Tools.copyToClipboard(context, text);
|
||||
updateLastUsedAndFrequency(position, getRealIndex(position));
|
||||
|
@ -593,6 +614,44 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
alert.show();
|
||||
}
|
||||
|
||||
public void establishPIN(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.setRawInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD | InputType.TYPE_CLASS_NUMBER);
|
||||
input.setText(displayedEntries.get(pos).getPin());
|
||||
input.setSingleLine();
|
||||
input.requestFocus();
|
||||
input.setTransformationMethod(new PasswordTransformationMethod());
|
||||
UIHelper.showKeyboard(context, input, true);
|
||||
|
||||
FrameLayout container = new FrameLayout(context);
|
||||
container.setPaddingRelative(marginMedium, marginSmall, marginMedium, 0);
|
||||
container.addView(input);
|
||||
|
||||
builder.setTitle(R.string.dialog_title_pin)
|
||||
.setCancelable(false)
|
||||
.setView(container)
|
||||
.setPositiveButton(R.string.button_accept, (dialogInterface, i) -> {
|
||||
int realIndex = getRealIndex(pos);
|
||||
String newPin = input.getEditableText().toString();
|
||||
|
||||
displayedEntries.get(pos).setPin(newPin);
|
||||
Entry e = entries.getEntry(realIndex);
|
||||
e.setPin(newPin);
|
||||
e.updateOTP(true);
|
||||
notifyItemChanged(pos);
|
||||
UIHelper.hideKeyboard(context, input);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> UIHelper.hideKeyboard(context, input))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
public void removeItem(final int pos) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
|
@ -650,6 +709,11 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
MenuInflater inflate = popup.getMenuInflater();
|
||||
inflate.inflate(R.menu.menu_popup, popup.getMenu());
|
||||
|
||||
if (displayedEntries.get(pos).getType() == Entry.OTPType.MOTP){
|
||||
MenuItem item = popup.getMenu().getItem(ESTABLISH_PIN_MENU_INDEX);
|
||||
item.setVisible(true);
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener(item -> {
|
||||
int id = item.getItemId();
|
||||
|
||||
|
@ -659,6 +723,9 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
|||
} else if(id == R.id.menu_popup_changeImage) {
|
||||
changeThumbnail(pos);
|
||||
return true;
|
||||
} else if (id == R.id.menu_popup_establishPin) {
|
||||
establishPIN(pos);
|
||||
return true;
|
||||
} else if (id == R.id.menu_popup_remove) {
|
||||
removeItem(pos);
|
||||
return true;
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_margin_xxsmall"
|
||||
android:layout_marginBottom="@dimen/activity_margin_xxsmall"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
app:contentPadding="0dp"
|
||||
style="?attr/cardStyle">
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_margin_xsmall"
|
||||
android:layout_marginBottom="@dimen/activity_margin_xsmall"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
app:contentPadding="0dp"
|
||||
style="?attr/cardStyle">
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
android:layout_marginTop="@dimen/activity_margin_xsmall"
|
||||
android:layout_marginBottom="@dimen/activity_margin_xsmall"
|
||||
app:contentPadding="0dp"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
style="?attr/cardStyle">
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -13,6 +13,11 @@
|
|||
android:id="@+id/menu_popup_show_qr_code"
|
||||
android:title="@string/menu_popup_show_qr_code" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_popup_establishPin"
|
||||
android:title="@string/menu_popup_establish_pin"
|
||||
android:visible="false"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_popup_remove"
|
||||
android:title="@string/menu_popup_remove" />
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<string name="button_all_tags">All tags</string>
|
||||
<string name="button_no_tags">No tags</string>
|
||||
<string name="button_qr_from_image">QR code from image</string>
|
||||
<string name="button_accept">Accept</string>
|
||||
|
||||
<!-- Custom formatting -->
|
||||
<string name="format_custom_period">%d s</string>
|
||||
|
@ -56,6 +57,7 @@
|
|||
<string name="menu_popup_change_image">Change image</string>
|
||||
<string name="menu_popup_remove">Remove</string>
|
||||
<string name="menu_popup_show_qr_code">Show QR Code</string>
|
||||
<string name="menu_popup_establish_pin">Enter pin</string>
|
||||
|
||||
<!-- Buttons -->
|
||||
<string name="button_card_options">More options</string>
|
||||
|
@ -93,6 +95,7 @@
|
|||
<string name="dialog_title_used_tokens">Tokens usage</string>
|
||||
<string name="dialog_title_keystore_error">KeyStore error</string>
|
||||
<string name="dialog_title_qr_code">QR Code</string>
|
||||
<string name="dialog_title_pin">PIN</string>
|
||||
|
||||
<string name="dialog_title_enter_password">Enter password</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue