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 5f89058b..7c76f36b 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 @@ -275,6 +275,46 @@ public class Entry { return jsonObj; } + public Uri toUri() { + String type; + switch (this.type) { + case TOTP: + type = "totp"; + break; + case HOTP: + type = "hotp"; + break; + default: + return null; + } + Uri.Builder builder = new Uri.Builder() + .scheme("otpauth") + .authority(type) + .appendPath(this.label) + .appendQueryParameter("secret", new Base32().encodeAsString(this.secret)); + if (this.issuer != null) { + builder.appendQueryParameter("issuer", this.issuer); + } + switch (this.type) { + case HOTP: + builder.appendQueryParameter("counter", Long.toString(this.counter)); + case TOTP: + if (this.period != TokenCalculator.TOTP_DEFAULT_PERIOD) + builder.appendQueryParameter("period", Integer.toString(this.period)); + break; + } + if (this.digits != TokenCalculator.TOTP_DEFAULT_DIGITS) { + builder.appendQueryParameter("digits", Integer.toString(this.digits)); + } + if (this.algorithm != TokenCalculator.DEFAULT_ALGORITHM) { + builder.appendQueryParameter("algorithm", this.algorithm.name()); + } + for (String tag : this.tags) { + builder.appendQueryParameter("tags", tag); + } + return builder.build(); + } + public boolean isTimeBased() { return type == OTPType.TOTP || type == OTPType.STEAM; } 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 6479bdc6..eb1872b9 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,8 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Handler; import androidx.annotation.NonNull; @@ -46,9 +48,13 @@ import android.widget.Filter; import android.widget.Filterable; import android.widget.FrameLayout; import android.widget.GridView; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; +import com.google.zxing.BarcodeFormat; +import com.journeyapps.barcodescanner.BarcodeEncoder; + import org.shadowice.flocke.andotp.Activities.MainActivity; import org.shadowice.flocke.andotp.Database.Entry; import org.shadowice.flocke.andotp.Dialogs.TagsDialog; @@ -681,6 +687,35 @@ public class EntriesCardAdapter extends RecyclerView.Adapter .show(); } + private void showQRCode(final int pos) { + Uri uri = displayedEntries.get(pos).toUri(); + if (uri != null) { + Bitmap bitmap; + try { + bitmap = new BarcodeEncoder().encodeBitmap(uri.toString(), BarcodeFormat.QR_CODE, 0, 0); + } catch(Exception ignored) { + Toast.makeText(context, R.string.toast_qr_failed_to_generate, Toast.LENGTH_LONG).show(); + return; + } + BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); + drawable.setFilterBitmap(false); + + ImageView image = new ImageView(context); + image.setAdjustViewBounds(true); + image.setScaleType(ImageView.ScaleType.FIT_CENTER); + image.setImageDrawable(drawable); + + new AlertDialog.Builder(context) + .setTitle(R.string.dialog_title_qr_code) + .setPositiveButton(android.R.string.ok, (dialog, which) -> {}) + .setView(image) + .create() + .show(); + } else { + Toast.makeText(context, R.string.toast_qr_unsuported, Toast.LENGTH_LONG).show(); + } + } + private void showPopupMenu(View view, final int pos) { View menuItemView = view.findViewById(R.id.menuButton); PopupMenu popup = new PopupMenu(view.getContext(), menuItemView); @@ -707,6 +742,9 @@ public class EntriesCardAdapter extends RecyclerView.Adapter } else if (id == R.id.menu_popup_remove) { removeItem(pos); return true; + } else if (id == R.id.menu_popup_show_qr_code) { + showQRCode(pos); + return true; } else { return false; } diff --git a/app/src/main/res/menu/menu_popup.xml b/app/src/main/res/menu/menu_popup.xml index fe27744d..50e6462a 100644 --- a/app/src/main/res/menu/menu_popup.xml +++ b/app/src/main/res/menu/menu_popup.xml @@ -20,4 +20,8 @@ android:id="@+id/menu_popup_remove" android:title="@string/menu_popup_remove" /> + + \ 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 12ae672c..772d6aff 100644 --- a/app/src/main/res/values/strings_main.xml +++ b/app/src/main/res/values/strings_main.xml @@ -57,6 +57,7 @@ Change image Edit tags Remove + Show QR Code Authentication failed, please try again! @@ -71,6 +72,8 @@ Could not find/confirm QR code Checksum verification failed while decoding QR code Format error in QR code + QR Code not supported + Failed to generate QR Code Authenticate @@ -80,6 +83,7 @@ Counter Used tokens KeyStore error + QR Code Enter password