diff --git a/app/build.gradle b/app/build.gradle index 79dffa3c..960f4b77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -103,6 +103,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation "androidx.biometric:biometric:1.0.1" implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e8ce3a2e..ce16d47d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,8 +6,12 @@ - - + + + diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java index f7eb9336..4c42f094 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java @@ -8,10 +8,11 @@ import com.beemdevelopment.aegis.encoding.Base64; import com.beemdevelopment.aegis.encoding.EncodingException; import com.google.protobuf.InvalidProtocolBufferException; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; -public class GoogleAuthInfo { +public class GoogleAuthInfo implements Serializable { public static final String SCHEME = "otpauth"; public static final String SCHEME_EXPORT = "otpauth-migration"; diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 46aff94a..12b6a9c9 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -745,6 +745,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene mode.finish(); return true; + case R.id.action_share_qr: + Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class); + ArrayList authInfos = new ArrayList<>(); + for (VaultEntry entry : _selectedEntries) { + GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer()); + authInfos.add(authInfo); + } + + intent.putExtra("authInfos", authInfos); + startActivity(intent); + + mode.finish(); + return true; + case R.id.action_delete: Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> { deleteEntries(_selectedEntries); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/TransferEntriesActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/TransferEntriesActivity.java new file mode 100644 index 00000000..d73dd441 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/TransferEntriesActivity.java @@ -0,0 +1,136 @@ +package com.beemdevelopment.aegis.ui; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.otp.GoogleAuthInfo; +import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultManager; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class TransferEntriesActivity extends AegisActivity { + private List _authInfos; + private ImageView _qrImage; + private TextView _issuer; + private TextView _accountName; + private TextView _entriesCount; + private Button _nextButton; + private Button _previousButton; + + private VaultManager _vault; + private int _currentEntryCount = 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_share_entry); + _vault = getApp().getVaultManager(); + + _qrImage = findViewById(R.id.ivQrCode); + _issuer = findViewById(R.id.tvIssuer); + _accountName = findViewById(R.id.tvAccountName); + _entriesCount = findViewById(R.id.tvEntriesCount); + _nextButton = findViewById(R.id.btnNext); + _previousButton = findViewById(R.id.btnPrevious); + + if (getSupportActionBar() != null){ + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + Intent intent = getIntent(); + _authInfos = (ArrayList) intent.getSerializableExtra("authInfos"); + + int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE; + _entriesCount.setVisibility(controlVisibility); + _nextButton.setVisibility(controlVisibility); + + _nextButton.setOnClickListener(v -> { + if (_currentEntryCount < _authInfos.size()) { + _previousButton.setVisibility(View.VISIBLE); + _currentEntryCount++; + generateQR(); + + if (_currentEntryCount == _authInfos.size()) { + _nextButton.setText(R.string.done); + } + } else { + finish(); + } + }); + + _previousButton.setOnClickListener(v -> { + if (_currentEntryCount > 1 ) { + _nextButton.setText(R.string.next); + _currentEntryCount--; + generateQR(); + + if (_currentEntryCount == 1) { + _previousButton.setVisibility(View.INVISIBLE); + } + } + }); + + generateQR(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + break; + default: + return super.onOptionsItemSelected(item); + } + + return true; + } + + + private void generateQR() { + GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1); + _issuer.setText(selectedEntry.getIssuer()); + _accountName.setText(selectedEntry.getAccountName()); + _entriesCount.setText(String.format(getString(R.string.entries_count), _currentEntryCount, _authInfos.size())); + + QRCodeWriter writer = new QRCodeWriter(); + BitMatrix bitMatrix = null; + try { + bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512); + } catch (WriterException e) { + Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e); + return; + } + + int width = bitMatrix.getWidth(); + int height = bitMatrix.getHeight(); + int[] pixels = new int[width * height]; + for (int y = 0; y < height; y++) { + int offset = y * width; + for (int x = 0; x < width; x++) { + pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : Color.WHITE; + } + } + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, width, 0, 0, width, height); + _qrImage.setImageBitmap(bitmap); + } +} diff --git a/app/src/main/res/drawable/ic_qr_code_full.xml b/app/src/main/res/drawable/ic_qr_code_full.xml new file mode 100644 index 00000000..9b5e128f --- /dev/null +++ b/app/src/main/res/drawable/ic_qr_code_full.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_share_entry.xml b/app/src/main/res/layout/activity_share_entry.xml new file mode 100644 index 00000000..bc3c7dc4 --- /dev/null +++ b/app/src/main/res/layout/activity_share_entry.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + +