package com.greenaddress.greenbits.ui; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.Typeface; import android.nfc.NdefMessage; import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.support.design.widget.Snackbar; import android.text.Editable; import android.text.Spannable; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.TextView; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.blockstream.libwally.Wally; import com.dd.CircularProgressButton; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.greenaddress.greenapi.CryptoHelper; import com.greenaddress.greenapi.LoginData; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import de.schildbach.wallet.ui.ScanActivity; public class MnemonicActivity extends LoginActivity { private static final String TAG = MnemonicActivity.class.getSimpleName(); private static final int PINSAVE = 1337; private static final int QRSCANNER = 1338; private static final int CAMERA_PERMISSION = 150; private ArrayList<String> mWordsArray; private Set<String> mWords; private EditText mMnemonicText; private CircularProgressButton mOkButton; private void showErrorCorrection(final String closeWord, final String badWord) { if (closeWord == null) return; final Snackbar snackbar = Snackbar .make(mMnemonicText, getString(R.string.invalidWord, badWord, closeWord), Snackbar.LENGTH_LONG) .setAction("Correct", new View.OnClickListener() { @Override public void onClick(final View v) { mMnemonicText.setOnTouchListener(null); final String mnemonicStr = UI.getText(mMnemonicText).replace(badWord, closeWord); mMnemonicText.setText(mnemonicStr); final int textLength = mnemonicStr.length(); mMnemonicText.setSelection(textLength, textLength); final int words = mnemonicStr.trim().split(" ").length; if (validateMnemonic(mnemonicStr) && (words == 24 || words == 27)) login(); } }); final View snackbarView = snackbar.getView(); snackbarView.setBackgroundColor(Color.DKGRAY); final TextView textView = UI.find(snackbarView, android.support.design.R.id.snackbar_text); textView.setTextColor(Color.WHITE); snackbar.show(); } private boolean validateMnemonic(final String mnemonic) { try { Wally.bip39_mnemonic_validate(Wally.bip39_get_wordlist("en"), mnemonic); return true; } catch (final IllegalArgumentException e) { for (final String w : mnemonic.split(" ")) if (!mWords.contains(w)) { setWord(w); showErrorCorrection(MnemonicHelper.getClosestWord(mWordsArray, w), w); break; } return false; } } private void enableLogin() { mOkButton.setProgress(0); mMnemonicText.setEnabled(true); } private void login() { if (mOkButton.getProgress() != 0) return; if (mService.isLoggedIn()) { toast(R.string.err_mnemonic_activity_logout_required); return; } if (!mService.isConnected()) { toast(R.string.err_send_not_connected_will_resume); return; } if (!validateMnemonic(UI.getText(mMnemonicText))) { toast(R.string.err_mnemonic_activity_invalid_mnemonic); return; } mOkButton.setProgress(50); mMnemonicText.setEnabled(false); hideKeyboardFrom(mMnemonicText); final AsyncFunction<Void, LoginData> connectToLogin = new AsyncFunction<Void, LoginData>() { @Override public ListenableFuture<LoginData> apply(final Void input) { final String mnemonics = UI.getText(mMnemonicText).trim(); if (mnemonics.split(" ").length != 27) return mService.login(mnemonics); // Encrypted mnemonic return Futures.transform(askForPassphrase(), new AsyncFunction<String, LoginData>() { @Override public ListenableFuture<LoginData> apply(final String passphrase) { return mService.login(CryptoHelper.encrypted_mnemonic_to_mnemonic(mnemonics, passphrase)); } }); } }; final ListenableFuture<LoginData> loginFuture; loginFuture = Futures.transform(mService.onConnected, connectToLogin, mService.getExecutor()); Futures.addCallback(loginFuture, new FutureCallback<LoginData>() { @Override public void onSuccess(final LoginData result) { if (getCallingActivity() == null) { final Intent savePin = PinSaveActivity.createIntent(MnemonicActivity.this, mService.getMnemonics()); startActivityForResult(savePin, PINSAVE); } else { setResult(RESULT_OK); finishOnUiThread(); } } @Override public void onFailure(final Throwable t) { final boolean accountDoesntExist = t instanceof ClassCastException; final String message = accountDoesntExist ? "Account doesn't exist" : "Login failed"; t.printStackTrace(); MnemonicActivity.this.runOnUiThread(new Runnable() { public void run() { MnemonicActivity.this.toast(message); enableLogin(); } }); } }, mService.getExecutor()); } private ListenableFuture<String> askForPassphrase() { final SettableFuture<String> passphraseFuture = SettableFuture.create(); runOnUiThread(new Runnable() { public void run() { final View v = getLayoutInflater().inflate(R.layout.dialog_passphrase, null, false); final EditText passphraseValue = UI.find(v, R.id.passphraseValue); passphraseValue.requestFocus(); final MaterialDialog dialog = UI.popup(MnemonicActivity.this, "Encryption passphrase") .customView(v, true) .onPositive(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(final MaterialDialog dlg, final DialogAction which) { passphraseFuture.set(UI.getText(passphraseValue)); } }) .onNegative(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(final MaterialDialog dlg, final DialogAction which) { enableLogin(); } }).build(); UI.showDialog(dialog); } }); return passphraseFuture; } protected int getMainViewId() { return R.layout.activity_mnemonic; } public static void initWordList(final ArrayList<String> wordsArray, final Set<String> words) { final Object en = Wally.bip39_get_wordlist("en"); for (int i = 0; i < Wally.BIP39_WORDLIST_LEN; ++i) { wordsArray.add(Wally.bip39_get_word(en, i)); if (words != null) words.add(wordsArray.get(i)); } } @Override protected void onCreateWithService(final Bundle savedInstanceState) { Log.i(TAG, getIntent().getType() + ' ' + getIntent()); mWordsArray = new ArrayList<>(Wally.BIP39_WORDLIST_LEN); mWords = new HashSet<>(Wally.BIP39_WORDLIST_LEN); initWordList(mWordsArray, mWords); mMnemonicText = UI.find(this, R.id.mnemonicText); mOkButton = UI.find(this,R.id.mnemonicOkButton); mOkButton.setIndeterminateProgressMode(true); UI.mapClick(this, R.id.mnemonicOkButton, new View.OnClickListener() { public void onClick(final View v) { login(); } }); UI.mapClick(this, R.id.mnemonicScanIcon, new View.OnClickListener() { public void onClick(final View v) { //New Marshmallow permissions paradigm final String[] perms = {"android.permission.CAMERA"}; if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP_MR1 && checkSelfPermission(perms[0]) != PackageManager.PERMISSION_GRANTED) { requestPermissions(perms, CAMERA_PERMISSION); } else { final Intent scanner = new Intent(MnemonicActivity.this, ScanActivity.class); startActivityForResult(scanner, QRSCANNER); } } } ); mMnemonicText.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(final TextView v, final int actionId, final KeyEvent event) { if(event != null && KeyEvent.KEYCODE_ENTER == event.getKeyCode()) return true; if (actionId == EditorInfo.IME_ACTION_GO) { login(); return true; } return false; } }); mMnemonicText.addTextChangedListener(new UI.TextWatcher() { public void afterTextChanged(final Editable s) { final String mnemonic = s.toString(); if (mnemonic.startsWith(" ")) { s.replace(0, 1, ""); return; } final int space_idx = mnemonic.lastIndexOf(" "); if (space_idx != -1) { try { s.replace(space_idx, 2, " "); } catch (final IndexOutOfBoundsException ignore) { // seems caused by backspace on a physical keyboard via emulator // ideally we handle this better but this seems to work } return; } final String[] split = mnemonic.split(" "); final boolean endsWithSpace = mnemonic.endsWith(" "); final int lastElement = split.length - 1; for (int i = 0; i < split.length; ++i) { final String word = split[i]; // check for equality // not last or last but postponed by a space // otherwise just that it's the start of a word if (MnemonicHelper.isInvalidWord(mWordsArray, word, !(i == lastElement) || endsWithSpace)) { if (spans == null || !word.equals(spans.word)) setWord(word); return; } } mMnemonicText.setOnTouchListener(null); final Spans copy = spans; spans = null; if (copy != null) for (final Object span : copy.spans) s.removeSpan(span); } }); NFCIntentMnemonicLogin(); } private void loginOnUiThread(final String mnemonics) { if (mService.onConnected == null || mnemonics.equals(mService.getMnemonics())) return; CB.after(mService.onConnected, new CB.Op<Void>() { @Override public void onSuccess(final Void result) { runOnUiThread(new Runnable() { public void run() { login(); } }); } }); } private byte[] getNFCPayload(final Intent intent) { final Parcelable[] extra = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); return ((NdefMessage) extra[0]).getRecords()[0].getPayload(); } private void NFCIntentMnemonicLogin() { final Intent intent = getIntent(); if (intent == null || !NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) return; mMnemonicText.setTextColor(Color.WHITE); if (intent.getType().equals("x-gait/mnc")) { // Unencrypted NFC final String mnemonics = CryptoHelper.mnemonic_from_bytes(getNFCPayload(intent)); mMnemonicText.setText(mnemonics); loginOnUiThread(mnemonics); } else if (intent.getType().equals("x-ga/en")) // Encrypted NFC CB.after(askForPassphrase(), new CB.Op<String>() { @Override public void onSuccess(final String passphrase) { final String mnemonics = CryptoHelper.encrypted_mnemonic_to_mnemonic(getNFCPayload(intent), passphrase); mMnemonicText.setText(mnemonics); loginOnUiThread(mnemonics); } }); } private Spans spans; private void setWord(final String badWord) { mMnemonicText.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(final View v, final MotionEvent motionEvent) { showErrorCorrection(MnemonicHelper.getClosestWord(mWordsArray, badWord), badWord); return false; } }); final Spannable spannable = mMnemonicText.getText(); final String mnemonics = spannable.toString(); int start = 0; for (final String word: mnemonics.split(" ")) { if (word.equals(badWord)) break; start += word.length() + 1; } final int end = start + badWord.length(); if (spans != null) for (final Object o: spans.spans) spannable.removeSpan(o); spans = new Spans(badWord); for (final Object s: spans.spans) spannable.setSpan(s, start, end, 0); } static class Spans { final String word; final List<Object> spans = new ArrayList<>(4); Spans(final String word) { this.word = word; spans.add(new StyleSpan(Typeface.BOLD)); spans.add(new StyleSpan(Typeface.ITALIC)); spans.add(new UnderlineSpan()); spans.add(new ForegroundColorSpan(Color.RED)); } } @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case PINSAVE: onLoginSuccess(); break; case QRSCANNER: if (data != null && data.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT) != null) { mMnemonicText.setText(data.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT)); login(); } break; } } @Override public boolean onCreateOptionsMenu(final Menu menu) { // FIXME: Show connectivity status to user // Inflate the menu; this adds items to the action bar if it is present. // getMenuInflater().inflate(R.menu.mnemonic, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. return item.getItemId() == R.id.action_settings || super.onOptionsItemSelected(item); } @Override public void onRequestPermissionsResult(final int permsRequestCode, final String[] permissions, final int[] grantResults) { switch (permsRequestCode) { case CAMERA_PERMISSION: final boolean cameraPermissionGranted = grantResults[0]== PackageManager.PERMISSION_GRANTED; if (cameraPermissionGranted) { final Intent scanner = new Intent(MnemonicActivity.this, ScanActivity.class); startActivityForResult(scanner, QRSCANNER); } else shortToast(R.string.err_qrscan_requires_camera_permissions); } } }