PageRenderTime 35ms CodeModel.GetById 18ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

/app/src/main/java/info/blockchain/wallet/viewModel/PinEntryViewModel.java

https://gitlab.com/admin-github-cloud/My-Wallet-V3-Android
Java | 385 lines | 300 code | 67 blank | 18 comment | 49 complexity | 4d36b2222142e50b71ae7a3f1a78e706 MD5 | raw file
  1package info.blockchain.wallet.viewModel;
  2
  3import android.content.Intent;
  4import android.os.Bundle;
  5import android.support.annotation.NonNull;
  6import android.support.annotation.Nullable;
  7import android.support.annotation.StringRes;
  8import android.support.annotation.UiThread;
  9import android.support.annotation.VisibleForTesting;
 10import android.view.View;
 11import android.widget.TextView;
 12
 13import info.blockchain.wallet.callbacks.DialogButtonCallback;
 14import info.blockchain.wallet.datamanagers.AuthDataManager;
 15import info.blockchain.wallet.payload.PayloadManager;
 16import info.blockchain.wallet.util.AppUtil;
 17import info.blockchain.wallet.util.CharSequenceX;
 18import info.blockchain.wallet.util.PrefsUtil;
 19import info.blockchain.wallet.util.StringUtils;
 20import info.blockchain.wallet.util.ViewUtils;
 21import info.blockchain.wallet.view.helpers.ToastCustom;
 22
 23import java.util.ArrayList;
 24import java.util.List;
 25
 26import javax.inject.Inject;
 27
 28import piuk.blockchain.android.R;
 29import piuk.blockchain.android.di.Injector;
 30import rx.exceptions.Exceptions;
 31import rx.subscriptions.CompositeSubscription;
 32
 33import static info.blockchain.wallet.view.CreateWalletFragment.KEY_INTENT_EMAIL;
 34import static info.blockchain.wallet.view.CreateWalletFragment.KEY_INTENT_PASSWORD;
 35
 36public class PinEntryViewModel implements ViewModel {
 37
 38    private static final int PIN_LENGTH = 4;
 39    private static final int MAX_ATTEMPTS = 4;
 40
 41    private DataListener mDataListener;
 42    @Inject protected AuthDataManager mAuthDataManager;
 43    @Inject protected AppUtil mAppUtil;
 44    @Inject protected PrefsUtil mPrefsUtil;
 45    @Inject protected PayloadManager mPayloadManager;
 46    @Inject protected StringUtils mStringUtils;
 47    @VisibleForTesting CompositeSubscription mCompositeSubscription;
 48
 49    private String email;
 50    private CharSequenceX password;
 51    @VisibleForTesting String mUserEnteredPin = "";
 52    @VisibleForTesting String mUserEnteredConfirmationPin;
 53    @VisibleForTesting boolean bAllowExit = true;
 54
 55    public interface DataListener {
 56
 57        Intent getPageIntent();
 58
 59        TextView[] getPinBoxArray();
 60
 61        void showProgressDialog(@StringRes int messageId, @Nullable String suffix);
 62
 63        void showToast(@StringRes int message, @ToastCustom.ToastType String toastType);
 64
 65        void dismissProgressDialog();
 66
 67        void showMaxAttemptsDialog();
 68
 69        void showValidationDialog();
 70
 71        void showCommonPinWarning(DialogButtonCallback callback);
 72
 73        void showWalletVersionNotSupportedDialog(int walletVersion);
 74
 75        void goToUpgradeWalletActivity();
 76
 77        void restartPage();
 78
 79        void setTitleString(@StringRes int title);
 80
 81        void setTitleVisibility(@ViewUtils.Visibility int visibility);
 82
 83        void clearPinBoxes();
 84    }
 85
 86    public PinEntryViewModel(DataListener listener) {
 87        Injector.getInstance().getAppComponent().inject(this);
 88        mDataListener = listener;
 89        mCompositeSubscription = new CompositeSubscription();
 90    }
 91
 92    public void onViewReady() {
 93        if (mDataListener.getPageIntent() != null) {
 94            Bundle extras = mDataListener.getPageIntent().getExtras();
 95            if (extras != null) {
 96                if (extras.containsKey(KEY_INTENT_EMAIL)) {
 97                    email = extras.getString(KEY_INTENT_EMAIL);
 98                }
 99
100                if (extras.containsKey(KEY_INTENT_PASSWORD)) {
101                    //noinspection ConstantConditions
102                    password = new CharSequenceX(extras.getString(KEY_INTENT_PASSWORD));
103                }
104
105                if (password != null && password.length() > 0 && email != null && !email.isEmpty()) {
106                    // Previous page was CreateWalletFragment
107                    bAllowExit = false;
108                    saveLoginAndPassword();
109                    mDataListener.showProgressDialog(R.string.create_wallet, "...");
110                    createWallet();
111                }
112            }
113        }
114
115        checkPinFails();
116    }
117
118    public void onDeleteClicked() {
119        if (mUserEnteredPin.length() > 0) {
120            // Remove last char from pin string
121            mUserEnteredPin = mUserEnteredPin.substring(0, mUserEnteredPin.length() - 1);
122
123            // Clear last box
124            mDataListener.getPinBoxArray()[mUserEnteredPin.length()].setBackgroundResource(R.drawable.rounded_view_blue_white_border);
125        }
126    }
127
128    public void padClicked(View view) {
129        if (mUserEnteredPin.length() == PIN_LENGTH) {
130            return;
131        }
132
133        // Append tapped #
134        mUserEnteredPin = mUserEnteredPin + view.getTag().toString().substring(0, 1);
135        mDataListener.getPinBoxArray()[mUserEnteredPin.length() - 1].setBackgroundResource(R.drawable.rounded_view_dark_blue);
136
137        // Perform appropriate action if PIN_LENGTH has been reached
138        if (mUserEnteredPin.length() == PIN_LENGTH) {
139
140            // Throw error on '0000' to avoid server-side type issue
141            if (mUserEnteredPin.equals("0000")) {
142                showErrorToast(R.string.zero_pin);
143                clearPinViewAndReset();
144                return;
145            }
146
147            // Only show warning on first entry and if user is creating a new PIN
148            if (isCreatingNewPin() && isPinCommon(mUserEnteredPin) && mUserEnteredConfirmationPin == null) {
149                mDataListener.showCommonPinWarning(new DialogButtonCallback() {
150                    @Override
151                    public void onPositiveClicked() {
152                        clearPinViewAndReset();
153                    }
154
155                    @Override
156                    public void onNegativeClicked() {
157                        validateAndConfirmPin();
158                    }
159                });
160            } else {
161                validateAndConfirmPin();
162            }
163        }
164    }
165
166    private void validateAndConfirmPin() {
167        // Validate
168        if (!mPrefsUtil.getValue(PrefsUtil.KEY_PIN_IDENTIFIER, "").isEmpty()) {
169            mDataListener.setTitleVisibility(View.INVISIBLE);
170            validatePIN(mUserEnteredPin);
171        } else if (mUserEnteredConfirmationPin == null) {
172            // End of Create -  Change to Confirm
173            mUserEnteredConfirmationPin = mUserEnteredPin;
174            mUserEnteredPin = "";
175            mDataListener.setTitleString(R.string.confirm_pin);
176            clearPinBoxes();
177        } else if (mUserEnteredConfirmationPin.equals(mUserEnteredPin)) {
178            // End of Confirm - Pin is confirmed
179            createNewPin(mUserEnteredPin);
180        } else {
181            // End of Confirm - Pin Mismatch
182            showErrorToast(R.string.pin_mismatch_error);
183            mDataListener.setTitleString(R.string.create_pin);
184            clearPinViewAndReset();
185        }
186    }
187
188    private void clearPinViewAndReset() {
189        clearPinBoxes();
190        mUserEnteredConfirmationPin = null;
191    }
192
193    public void clearPinBoxes() {
194        mUserEnteredPin = "";
195        mDataListener.clearPinBoxes();
196    }
197
198    @SuppressWarnings("WeakerAccess")
199    @VisibleForTesting
200    void updatePayload(CharSequenceX password) {
201        mDataListener.showProgressDialog(R.string.decrypting_wallet, null);
202
203        mCompositeSubscription.add(
204                mAuthDataManager.updatePayload(
205                        mPrefsUtil.getValue(PrefsUtil.KEY_SHARED_KEY, ""),
206                        mPrefsUtil.getValue(PrefsUtil.KEY_GUID, ""),
207                        password)
208                        .doOnTerminate(() -> mDataListener.dismissProgressDialog())
209                        .subscribe(aVoid -> {
210                            mAppUtil.setSharedKey(mPayloadManager.getPayload().getSharedKey());
211
212                            double walletVersion = mPayloadManager.getVersion();
213
214                            if (walletVersion > PayloadManager.SUPPORTED_ENCRYPTION_VERSION) {
215                                mDataListener.showWalletVersionNotSupportedDialog((int) walletVersion);
216                            } else {
217                                setAccountLabelIfNecessary();
218
219                                if (!mPayloadManager.getPayload().isUpgraded()) {
220                                    mDataListener.goToUpgradeWalletActivity();
221                                } else {
222                                    mAppUtil.restartAppWithVerifiedPin();
223                                }
224                            }
225
226                        }, throwable -> mAppUtil.clearCredentialsAndRestart()));
227    }
228
229    public void validatePassword(CharSequenceX password) {
230        mDataListener.showProgressDialog(R.string.validating_password, null);
231
232        mCompositeSubscription.add(
233                mAuthDataManager.updatePayload(
234                        mPrefsUtil.getValue(PrefsUtil.KEY_SHARED_KEY, ""),
235                        mPrefsUtil.getValue(PrefsUtil.KEY_GUID, ""),
236                        password)
237                        .doOnSubscribe(() -> mPayloadManager.setTempPassword(new CharSequenceX("")))
238                        .subscribe(o -> {
239                            mDataListener.showToast(R.string.pin_4_strikes_password_accepted, ToastCustom.TYPE_OK);
240                            mPrefsUtil.removeValue(PrefsUtil.KEY_PIN_FAILS);
241                            mPrefsUtil.removeValue(PrefsUtil.KEY_PIN_IDENTIFIER);
242                            mDataListener.restartPage();
243                            mDataListener.dismissProgressDialog();
244                        }, throwable -> {
245                            showErrorToast(R.string.invalid_password);
246                            mDataListener.showValidationDialog();
247                        }));
248    }
249
250    private void createNewPin(String pin) {
251        mDataListener.showProgressDialog(R.string.creating_pin, null);
252
253        mCompositeSubscription.add(
254                mAuthDataManager.createPin(mPayloadManager.getTempPassword(), pin)
255                        .subscribe(createSuccessful -> {
256                            mDataListener.dismissProgressDialog();
257                            if (createSuccessful) {
258                                mPrefsUtil.setValue(PrefsUtil.KEY_PIN_FAILS, 0);
259                                updatePayload(mPayloadManager.getTempPassword());
260                            } else {
261                                throw Exceptions.propagate(new Throwable("Pin create failed"));
262                            }
263                        }, throwable -> {
264                            showErrorToast(R.string.create_pin_failed);
265                            mPrefsUtil.clear();
266                            mAppUtil.restartApp();
267                        }));
268    }
269
270    private void validatePIN(String pin) {
271        mDataListener.showProgressDialog(R.string.validating_pin, null);
272
273        mAuthDataManager.validatePin(pin)
274                .subscribe(password -> {
275                    mDataListener.dismissProgressDialog();
276                    if (password != null) {
277                        mPrefsUtil.setValue(PrefsUtil.KEY_PIN_FAILS, 0);
278                        updatePayload(password);
279                    } else {
280                        incrementFailureCount();
281                    }
282                }, throwable -> {
283                    showErrorToast(R.string.unexpected_error);
284                    mDataListener.restartPage();
285                });
286    }
287
288    public void incrementFailureCount() {
289        int fails = mPrefsUtil.getValue(PrefsUtil.KEY_PIN_FAILS, 0);
290        mPrefsUtil.setValue(PrefsUtil.KEY_PIN_FAILS, ++fails);
291        showErrorToast(R.string.invalid_pin);
292        mDataListener.restartPage();
293    }
294
295    // Check user's password if PIN fails >= 4
296    private void checkPinFails() {
297        int fails = mPrefsUtil.getValue(PrefsUtil.KEY_PIN_FAILS, 0);
298        if (fails >= MAX_ATTEMPTS) {
299            showErrorToast(R.string.pin_4_strikes);
300            mPayloadManager.getPayload().stepNumber = 0;
301            mDataListener.showMaxAttemptsDialog();
302        }
303    }
304
305    private void saveLoginAndPassword() {
306        mPrefsUtil.setValue(PrefsUtil.KEY_EMAIL, email);
307        mPayloadManager.setEmail(email);
308        mPayloadManager.setTempPassword(password);
309    }
310
311    private void setAccountLabelIfNecessary() {
312        if (mAppUtil.isNewlyCreated()
313                && mPayloadManager.getPayload().getHdWallet() != null
314                && (mPayloadManager.getPayload().getHdWallet().getAccounts().get(0).getLabel() == null
315                || mPayloadManager.getPayload().getHdWallet().getAccounts().get(0).getLabel().isEmpty())) {
316
317            mPayloadManager.getPayload().getHdWallet().getAccounts().get(0).setLabel(mStringUtils.getString(R.string.default_wallet_name));
318        }
319    }
320
321    private void createWallet() {
322        mCompositeSubscription.add(
323                mAuthDataManager.createHdWallet(password.toString(), mStringUtils.getString(R.string.default_wallet_name))
324                        .doAfterTerminate(() -> mDataListener.dismissProgressDialog())
325                        .subscribe(payload -> {
326                            if (payload != null) {
327                                // Successfully created and saved
328                                mPrefsUtil.setValue(PrefsUtil.KEY_GUID, payload.getGuid());
329                                mAppUtil.setSharedKey(payload.getSharedKey());
330                            } else {
331                                showErrorToast(R.string.remote_save_ko);
332                            }
333                        }, throwable -> showErrorToastAndRestartApp(R.string.hd_error)));
334    }
335
336    private boolean isPinCommon(String pin) {
337        List<String> commonPins = new ArrayList<String>() {{
338            add("1234");
339            add("1111");
340            add("1212");
341            add("7777");
342            add("1004");
343        }};
344        return commonPins.contains(pin);
345    }
346
347    public void resetApp() {
348        mAppUtil.clearCredentialsAndRestart();
349    }
350
351    public boolean allowExit() {
352        return bAllowExit;
353    }
354
355    public boolean isCreatingNewPin() {
356        return mPrefsUtil.getValue(PrefsUtil.KEY_PIN_IDENTIFIER, "").isEmpty();
357    }
358
359    @UiThread
360    private void showErrorToast(@StringRes int message) {
361        mDataListener.dismissProgressDialog();
362        mDataListener.showToast(message, ToastCustom.TYPE_ERROR);
363    }
364
365    @UiThread
366    private void showErrorToastAndRestartApp(@StringRes int message) {
367        mDataListener.dismissProgressDialog();
368        mDataListener.showToast(message, ToastCustom.TYPE_ERROR);
369        resetApp();
370    }
371
372    @NonNull
373    public AppUtil getAppUtil() {
374        return mAppUtil;
375    }
376
377    @Override
378    public void destroy() {
379        // Clear all subscriptions so that:
380        // 1) all processes stop and no memory is leaked
381        // 2) processes don't try to update a null View
382        // 3) background processes don't leak memory
383        mCompositeSubscription.clear();
384    }
385}