package com.mygeopay.wallet.ui; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageButton; import android.widget.MultiAutoCompleteTextView; import android.widget.TextView; import com.mygeopay.wallet.Constants; import com.mygeopay.wallet.R; import com.mygeopay.wallet.util.Fonts; import com.mygeopay.wallet.util.Keyboard; import com.mygeopay.wallet.util.PasswordQualityChecker; import org.bitcoinj.crypto.MnemonicCode; import org.bitcoinj.crypto.MnemonicException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import javax.annotation.Nullable; import static com.mygeopay.core.Preconditions.checkState; /** * A simple {@link Fragment} subclass. * */ public class RestoreFragment extends Fragment { private static final Logger log = LoggerFactory.getLogger(RestoreFragment.class); private static final int REQUEST_CODE_SCAN = 0; private MultiAutoCompleteTextView mnemonicTextView; @Nullable private String seed; private boolean isNewSeed; private TextView errorMnemonicΜessage; private int colorSignificant; private int colorInsignificant; private int colorError; private WelcomeFragment.Listener mListener; private boolean isSeedProtected = false; private TextView errorPassword; private TextView errorPasswordsMismatch; private EditText password1; private EditText password2; private PasswordQualityChecker passwordQualityChecker; private Button skipButton; public static RestoreFragment newInstance() { return newInstance(null); } public static RestoreFragment newInstance(@Nullable String seed) { RestoreFragment fragment = new RestoreFragment(); if (seed != null) { Bundle args = new Bundle(); args.putString(Constants.ARG_SEED, seed); fragment.setArguments(args); } return fragment; } public RestoreFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { seed = getArguments().getString(Constants.ARG_SEED); isNewSeed = seed != null; } passwordQualityChecker = new PasswordQualityChecker(getActivity()); colorInsignificant = getResources().getColor(R.color.gray_26_hint_text); colorError = getResources().getColor(R.color.fg_error); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_restore, container, false); Fonts.setTypeface(view.findViewById(R.id.coins_icon), Fonts.Font.COINOMI_FONT_ICONS); ImageButton scanQrButton = (ImageButton) view.findViewById(R.id.scan_qr_code); scanQrButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { handleScan(); } }); // Setup auto complete the mnemonic words ArrayAdapter<String> adapter = new ArrayAdapter<String>(this.getActivity(), R.layout.item_simple, MnemonicCode.INSTANCE.getWordList()); mnemonicTextView = (MultiAutoCompleteTextView) view.findViewById(R.id.seed); mnemonicTextView.setAdapter(adapter); mnemonicTextView.setTokenizer(new SpaceTokenizer() { @Override public void onToken() { clearError(errorMnemonicΜessage); } }); // Restore message errorMnemonicΜessage = (TextView) view.findViewById(R.id.restore_message); errorMnemonicΜessage.setVisibility(View.GONE); // Password protected seed (BIP39) // Type and retype password errorPassword = (TextView) view.findViewById(R.id.password_error); errorPasswordsMismatch = (TextView) view.findViewById(R.id.passwords_mismatch); clearError(errorPassword); clearError(errorPasswordsMismatch); password1 = (EditText) view.findViewById(R.id.password1); password2 = (EditText) view.findViewById(R.id.password2); final View password1Title = view.findViewById(R.id.password1_title); final View password2Title = view.findViewById(R.id.password2_title); password1.setVisibility(View.GONE); password2.setVisibility(View.GONE); password1Title.setVisibility(View.GONE); password2Title.setVisibility(View.GONE); password1.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View textView, boolean hasFocus) { if (hasFocus) { clearError(errorPassword); } else { checkPasswordQuality(); } } }); password2.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View textView, boolean hasFocus) { if (hasFocus) { clearError(errorPasswordsMismatch); } else { checkPasswordsMatch(); } } }); // Checkbox to enable/disable password protected seed (BIP39) // For new seed final TextView seedProtectInfoNew = (TextView) view.findViewById(R.id.seed_protect_info); seedProtectInfoNew.setVisibility(View.GONE); CheckBox seedProtectNew = (CheckBox) view.findViewById(R.id.seed_protect); if (!isNewSeed) seedProtectNew.setVisibility(View.GONE); // For existing seed final TextView seedProtectInfoExisting = (TextView) view.findViewById(R.id.restore_seed_protected_info); seedProtectInfoExisting.setVisibility(View.GONE); final CheckBox seedProtectExisting = (CheckBox) view.findViewById(R.id.restore_seed_protected); if (isNewSeed) seedProtectExisting.setVisibility(View.GONE); // Generic checkbox and info text final TextView seedProtectInfo; final CheckBox seedProtect; if (isNewSeed) { seedProtectInfo = seedProtectInfoNew; seedProtect = seedProtectNew; } else { seedProtectInfo = seedProtectInfoExisting; seedProtect = seedProtectExisting; } seedProtect.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { isSeedProtected = isChecked; if (isChecked) { skipButton.setVisibility(View.GONE); seedProtectInfo.setVisibility(View.VISIBLE); password1Title.setVisibility(View.VISIBLE); password1.setVisibility(View.VISIBLE); if (isNewSeed) { password2Title.setVisibility(View.VISIBLE); password2.setVisibility(View.VISIBLE); } else { password2Title.setVisibility(View.GONE); password2.setVisibility(View.GONE); } } else { skipButton.setVisibility(View.VISIBLE); seedProtectInfo.setVisibility(View.GONE); password1Title.setVisibility(View.GONE); password2Title.setVisibility(View.GONE); password1.setVisibility(View.GONE); password2.setVisibility(View.GONE); password1.setText(null); password2.setText(null); } clearError(errorPassword); clearError(errorPasswordsMismatch); } }); // Skip link skipButton = (Button) view.findViewById(R.id.seed_entry_skip); if (seed != null) { skipButton.setOnClickListener(getOnSkipListener()); skipButton.setVisibility(View.VISIBLE); } else { skipButton.setVisibility(View.GONE); } // Next button view.findViewById(R.id.button_next).setOnClickListener(getOnNextListener()); return view; } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (WelcomeFragment.Listener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement WelcomeFragment.OnFragmentInteractionListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } private boolean checkPassword() { boolean isPasswordValid = true; if (isNewSeed) { isPasswordValid = checkPasswordQuality() && checkPasswordsMatch(); } return isPasswordValid; } private boolean checkPasswordQuality() { String pass = password1.getText().toString(); boolean isPasswordGood = false; try { passwordQualityChecker.checkPassword(pass); isPasswordGood = true; clearError(errorPassword); } catch (PasswordQualityChecker.PasswordTooCommonException e1) { log.info("Entered a too common password {}", pass); setError(errorPassword, R.string.password_too_common_error, pass); } catch (PasswordQualityChecker.PasswordTooShortException e2) { log.info("Entered a too short password"); setError(errorPassword, R.string.password_too_short_error, passwordQualityChecker.getMinPasswordLength()); } log.info("Password good = {}", isPasswordGood); return isPasswordGood; } private boolean checkPasswordsMatch() { checkState(isNewSeed, "Cannot check if passwords match when restoring."); String pass1 = password1.getText().toString(); String pass2 = password2.getText().toString(); boolean isPasswordsMatch = pass1.equals(pass2); if (!isPasswordsMatch) showError(errorPasswordsMismatch); log.info("Passwords match = {}", isPasswordsMatch); return isPasswordsMatch; } private View.OnClickListener getOnNextListener() { return new View.OnClickListener() { @Override public void onClick(View v) { verifyMnemonicAndProceed(); } }; } private View.OnClickListener getOnSkipListener() { return new View.OnClickListener() { @Override public void onClick(View v) { log.info("Skipping seed verification."); mnemonicTextView.setText(""); SkipDialogFragment skipDialog = SkipDialogFragment.newInstance(seed); skipDialog.show(getFragmentManager(), null); } }; } private void verifyMnemonicAndProceed() { Keyboard.hideKeyboard(getActivity()); if (checkAllValid()) { Bundle args = getArguments(); if (args == null) args = new Bundle(); if (isSeedProtected) { args.putString(Constants.ARG_SEED_PASSWORD, password1.getText().toString()); } args.putString(Constants.ARG_SEED, mnemonicTextView.getText().toString().trim()); if (mListener != null) mListener.onSeedVerified(args); } } private boolean checkAllValid() { boolean isAllValid = verifyMnemonic(); if (isAllValid && isSeedProtected) { isAllValid = checkPassword(); } return isAllValid; } private boolean verifyMnemonic() { log.info("Verifying seed"); // TODO, use util class to be ported from the NXT branch String seedText = mnemonicTextView.getText().toString().trim(); ArrayList<String> seedWords = new ArrayList<>(); for (String word : seedText.trim().split(" ")) { if (word.isEmpty()) continue; seedWords.add(word); } boolean isSeedValid = false; try { MnemonicCode.INSTANCE.check(seedWords); clearError(errorMnemonicΜessage); isSeedValid = true; } catch (MnemonicException.MnemonicChecksumException e) { log.info("Checksum error in seed: {}", e.getMessage()); setError(errorMnemonicΜessage, R.string.restore_error_checksum); } catch (MnemonicException.MnemonicWordException e) { log.info("Unknown words in seed: {}", e.getMessage()); setError(errorMnemonicΜessage, R.string.restore_error_words); } catch (MnemonicException e) { log.info("Error verifying seed: {}", e.getMessage()); setError(errorMnemonicΜessage, R.string.restore_error, e.getMessage()); } if (isSeedValid && seed != null && !seedText.equals(seed.trim())) { log.info("Typed seed does not match the generated one."); setError(errorMnemonicΜessage, R.string.restore_error_mismatch); isSeedValid = false; } return isSeedValid; } public static class SkipDialogFragment extends DialogFragment { private WelcomeFragment.Listener mListener; public static SkipDialogFragment newInstance(String seed) { SkipDialogFragment newDialog = new SkipDialogFragment(); Bundle args = new Bundle(); args.putString(Constants.ARG_SEED, seed); newDialog.setArguments(args); return newDialog; } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (WelcomeFragment.Listener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement WelcomeFragment.OnFragmentInteractionListener"); } } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final String seed = getArguments().getString(Constants.ARG_SEED); // FIXME does not look good with custom dialogs in older Samsungs // View dialogView = getActivity().getLayoutInflater().inflate(R.layout.skip_seed_dialog, null); // TextView seedView = (TextView) dialogView.findViewById(R.id.seed); // seedView.setText(seed); String dialogMessage = getResources().getString(R.string.restore_skip_info1) + "\n\n" + seed + "\n\n" + getResources().getString(R.string.restore_skip_info2); // Use the Builder class for convenient dialog construction AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.restore_skip_title) // .setView(dialogView) FIXME .setMessage(dialogMessage) .setPositiveButton(R.string.button_skip, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (mListener != null) mListener.onSeedVerified(getArguments()); } }) .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dismiss(); } }); return builder.create(); } } private void setError(TextView errorView, int messageId, Object... formatArgs) { setError(errorView, getResources().getString(messageId, formatArgs)); } private void setError(TextView errorView, String message) { errorView.setText(message); showError(errorView); } private void showError(TextView errorView) { errorView.setVisibility(View.VISIBLE); } private void clearError(TextView errorView) { errorView.setVisibility(View.GONE); } private void handleScan() { startActivityForResult(new Intent(getActivity(), ScanActivity.class), REQUEST_CODE_SCAN); } @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { if (requestCode == REQUEST_CODE_SCAN && resultCode == Activity.RESULT_OK) { mnemonicTextView.setText(intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT)); verifyMnemonic(); } } private abstract static class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer { public int findTokenStart(CharSequence text, int cursor) { int i = cursor; while (i > 0 && text.charAt(i - 1) != ' ') { i--; } return i; } public int findTokenEnd(CharSequence text, int cursor) { int i = cursor; int len = text.length(); while (i < len) { if (text.charAt(i) == ' ') { return i; } else { i++; } } return len; } public CharSequence terminateToken(CharSequence text) { onToken(); return text + " "; } abstract public void onToken(); } }