VerifyActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN;
  4
  5import android.app.AlertDialog;
  6import android.content.ClipData;
  7import android.content.ClipDescription;
  8import android.content.ClipboardManager;
  9import android.content.Context;
 10import android.content.Intent;
 11import android.os.Bundle;
 12import android.os.Handler;
 13import android.os.SystemClock;
 14import android.text.Html;
 15import android.view.View;
 16import androidx.databinding.DataBindingUtil;
 17import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 18import com.google.android.material.snackbar.Snackbar;
 19import eu.siacs.conversations.R;
 20import eu.siacs.conversations.databinding.ActivityVerifyBinding;
 21import eu.siacs.conversations.entities.Account;
 22import eu.siacs.conversations.services.QuickConversationsService;
 23import eu.siacs.conversations.ui.util.ApiDialogHelper;
 24import eu.siacs.conversations.ui.util.PinEntryWrapper;
 25import eu.siacs.conversations.utils.AccountUtils;
 26import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 27import eu.siacs.conversations.utils.TimeFrameUtils;
 28import io.michaelrocks.libphonenumber.android.NumberParseException;
 29import java.util.concurrent.atomic.AtomicBoolean;
 30
 31public class VerifyActivity extends XmppActivity
 32        implements ClipboardManager.OnPrimaryClipChangedListener,
 33                QuickConversationsService.OnVerification,
 34                QuickConversationsService.OnVerificationRequested {
 35
 36    public static final String EXTRA_RETRY_SMS_AFTER = "retry_sms_after";
 37    private static final String EXTRA_RETRY_VERIFICATION_AFTER = "retry_verification_after";
 38    private final Handler mHandler = new Handler();
 39    private ActivityVerifyBinding binding;
 40    private Account account;
 41    private PinEntryWrapper pinEntryWrapper;
 42    private ClipboardManager clipboardManager;
 43    private String pasted = null;
 44    private boolean verifying = false;
 45    private boolean requestingVerification = false;
 46    private long retrySmsAfter = 0;
 47    private final Runnable SMS_TIMEOUT_UPDATER =
 48            new Runnable() {
 49                @Override
 50                public void run() {
 51                    if (setTimeoutLabelInResendButton()) {
 52                        mHandler.postDelayed(this, 300);
 53                    }
 54                }
 55            };
 56    private long retryVerificationAfter = 0;
 57    private final Runnable VERIFICATION_TIMEOUT_UPDATER =
 58            new Runnable() {
 59                @Override
 60                public void run() {
 61                    if (setTimeoutLabelInNextButton()) {
 62                        mHandler.postDelayed(this, 300);
 63                    }
 64                }
 65            };
 66    private final AtomicBoolean redirectInProgress = new AtomicBoolean(false);
 67
 68    private boolean setTimeoutLabelInResendButton() {
 69        if (retrySmsAfter != 0) {
 70            long remaining = retrySmsAfter - SystemClock.elapsedRealtime();
 71            if (remaining >= 0) {
 72                binding.resendSms.setEnabled(false);
 73                binding.resendSms.setText(
 74                        getString(
 75                                R.string.resend_sms_in,
 76                                TimeFrameUtils.resolve(VerifyActivity.this, remaining)));
 77                return true;
 78            }
 79        }
 80        binding.resendSms.setEnabled(true);
 81        binding.resendSms.setText(R.string.resend_sms);
 82        return false;
 83    }
 84
 85    private boolean setTimeoutLabelInNextButton() {
 86        if (retryVerificationAfter != 0) {
 87            long remaining = retryVerificationAfter - SystemClock.elapsedRealtime();
 88            if (remaining >= 0) {
 89                binding.next.setEnabled(false);
 90                binding.next.setText(
 91                        getString(
 92                                R.string.wait_x,
 93                                TimeFrameUtils.resolve(VerifyActivity.this, remaining)));
 94                return true;
 95            }
 96        }
 97        this.binding.next.setEnabled(!verifying);
 98        this.binding.next.setText(verifying ? R.string.verifying : R.string.next);
 99        return false;
100    }
101
102    @Override
103    protected void onCreate(final Bundle savedInstanceState) {
104        super.onCreate(savedInstanceState);
105        String pin = savedInstanceState != null ? savedInstanceState.getString("pin") : null;
106        boolean verifying =
107                savedInstanceState != null && savedInstanceState.getBoolean("verifying");
108        boolean requestingVerification =
109                savedInstanceState != null
110                        && savedInstanceState.getBoolean("requesting_verification", false);
111        this.pasted = savedInstanceState != null ? savedInstanceState.getString("pasted") : null;
112        this.retrySmsAfter =
113                savedInstanceState != null
114                        ? savedInstanceState.getLong(EXTRA_RETRY_SMS_AFTER, 0L)
115                        : 0L;
116        this.retryVerificationAfter =
117                savedInstanceState != null
118                        ? savedInstanceState.getLong(EXTRA_RETRY_VERIFICATION_AFTER, 0L)
119                        : 0L;
120        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_verify);
121        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
122        setSupportActionBar(this.binding.toolbar);
123        this.pinEntryWrapper = new PinEntryWrapper(binding.pinBox);
124        if (pin != null) {
125            this.pinEntryWrapper.setPin(pin);
126        }
127        binding.back.setOnClickListener(this::onBackButton);
128        binding.next.setOnClickListener(this::onNextButton);
129        binding.resendSms.setOnClickListener(this::onResendSmsButton);
130        clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
131        setVerifyingState(verifying);
132        setRequestingVerificationState(requestingVerification);
133    }
134
135    private void onBackButton(View view) {
136        if (this.verifying) {
137            setVerifyingState(false);
138            return;
139        }
140        final Intent intent = new Intent(this, EnterPhoneNumberActivity.class);
141        if (this.account != null) {
142            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
143            builder.setMessage(R.string.abort_registration_procedure);
144            builder.setPositiveButton(
145                    R.string.yes,
146                    (dialog, which) -> {
147                        xmppConnectionService.deleteAccount(account);
148                        startActivity(intent);
149                        finish();
150                    });
151            builder.setNegativeButton(R.string.no, null);
152            builder.create().show();
153        } else {
154            startActivity(intent);
155            finish();
156        }
157    }
158
159    private void onNextButton(View view) {
160        final String pin = pinEntryWrapper.getPin();
161        if (PinEntryWrapper.isValidPin(pin)) {
162            if (account != null && xmppConnectionService != null) {
163                setVerifyingState(true);
164                xmppConnectionService.getQuickConversationsService().verify(account, pin);
165            }
166        } else {
167            AlertDialog.Builder builder = new AlertDialog.Builder(this);
168            builder.setMessage(R.string.please_enter_pin);
169            builder.setPositiveButton(R.string.ok, null);
170            builder.create().show();
171        }
172    }
173
174    private void onResendSmsButton(View view) {
175        try {
176            xmppConnectionService
177                    .getQuickConversationsService()
178                    .requestVerification(
179                            PhoneNumberUtilWrapper.toPhoneNumber(this, account.getJid()));
180            setRequestingVerificationState(true);
181        } catch (NumberParseException e) {
182
183        }
184    }
185
186    private void setVerifyingState(boolean verifying) {
187        this.verifying = verifying;
188        this.binding.back.setText(verifying ? R.string.cancel : R.string.back);
189        this.binding.next.setEnabled(!verifying);
190        this.binding.next.setText(verifying ? R.string.verifying : R.string.next);
191        this.binding.resendSms.setVisibility(verifying ? View.GONE : View.VISIBLE);
192        pinEntryWrapper.setEnabled(!verifying);
193        this.binding.progressBar.setVisibility(verifying ? View.VISIBLE : View.GONE);
194        this.binding.progressBar.setIndeterminate(verifying);
195    }
196
197    private void setRequestingVerificationState(boolean requesting) {
198        this.requestingVerification = requesting;
199        if (requesting) {
200            this.binding.resendSms.setEnabled(false);
201            this.binding.resendSms.setText(R.string.requesting_sms);
202        } else {
203            setTimeoutLabelInResendButton();
204        }
205    }
206
207    @Override
208    protected void refreshUiReal() {}
209
210    @Override
211    public void onBackendConnected() {
212        xmppConnectionService.getQuickConversationsService().addOnVerificationListener(this);
213        xmppConnectionService
214                .getQuickConversationsService()
215                .addOnVerificationRequestedListener(this);
216        this.account = AccountUtils.getFirst(xmppConnectionService);
217        if (this.account == null) {
218            return;
219        }
220        if (!account.isOptionSet(Account.OPTION_UNVERIFIED)
221                && !account.isOptionSet(Account.OPTION_DISABLED)) {
222            runOnUiThread(this::performPostVerificationRedirect);
223            return;
224        }
225        this.binding.weHaveSent.setText(
226                Html.fromHtml(
227                        getString(
228                                R.string.we_have_sent_you_an_sms_to_x,
229                                PhoneNumberUtilWrapper.toFormattedPhoneNumber(
230                                        this, this.account.getJid()))));
231        setVerifyingState(xmppConnectionService.getQuickConversationsService().isVerifying());
232        setRequestingVerificationState(
233                xmppConnectionService.getQuickConversationsService().isRequestingVerification());
234    }
235
236    @Override
237    public void onSaveInstanceState(Bundle savedInstanceState) {
238        savedInstanceState.putString("pin", this.pinEntryWrapper.getPin());
239        savedInstanceState.putBoolean("verifying", this.verifying);
240        savedInstanceState.putBoolean("requesting_verification", this.requestingVerification);
241        savedInstanceState.putLong(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter);
242        savedInstanceState.putLong(EXTRA_RETRY_VERIFICATION_AFTER, this.retryVerificationAfter);
243        if (this.pasted != null) {
244            savedInstanceState.putString("pasted", this.pasted);
245        }
246        super.onSaveInstanceState(savedInstanceState);
247    }
248
249    @Override
250    public void onStart() {
251        super.onStart();
252        clipboardManager.addPrimaryClipChangedListener(this);
253        final Intent intent = getIntent();
254        this.retrySmsAfter =
255                intent != null
256                        ? intent.getLongExtra(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter)
257                        : this.retrySmsAfter;
258        if (this.retrySmsAfter > 0) {
259            mHandler.post(SMS_TIMEOUT_UPDATER);
260        }
261        if (this.retryVerificationAfter > 0) {
262            mHandler.post(VERIFICATION_TIMEOUT_UPDATER);
263        }
264    }
265
266    @Override
267    public void onStop() {
268        super.onStop();
269        mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER);
270        mHandler.removeCallbacks(VERIFICATION_TIMEOUT_UPDATER);
271        clipboardManager.removePrimaryClipChangedListener(this);
272        if (xmppConnectionService != null) {
273            xmppConnectionService.getQuickConversationsService().removeOnVerificationListener(this);
274            xmppConnectionService
275                    .getQuickConversationsService()
276                    .removeOnVerificationRequestedListener(this);
277        }
278    }
279
280    @Override
281    public void onResume() {
282        super.onResume();
283        if (pinEntryWrapper.isEmpty()) {
284            // starting with Android P we need input focus
285            pinEntryWrapper.requestFocus();
286            pastePinFromClipboard();
287        }
288    }
289
290    private void pastePinFromClipboard() {
291        final ClipDescription description =
292                clipboardManager != null ? clipboardManager.getPrimaryClipDescription() : null;
293        if (description != null && description.hasMimeType(MIMETYPE_TEXT_PLAIN)) {
294            final ClipData primaryClip = clipboardManager.getPrimaryClip();
295            if (primaryClip != null && primaryClip.getItemCount() > 0) {
296                final CharSequence clip = primaryClip.getItemAt(0).getText();
297                if (PinEntryWrapper.isValidPin(clip) && !clip.toString().equals(this.pasted)) {
298                    this.pasted = clip.toString();
299                    pinEntryWrapper.setPin(clip.toString());
300                    final Snackbar snackbar =
301                            Snackbar.make(
302                                    binding.coordinator,
303                                    R.string.possible_pin,
304                                    Snackbar.LENGTH_LONG);
305                    snackbar.setAction(R.string.undo, v -> pinEntryWrapper.clear());
306                    snackbar.show();
307                }
308            }
309        }
310    }
311
312    private void performPostVerificationRedirect() {
313        if (redirectInProgress.compareAndSet(false, true)) {
314            Intent intent = new Intent(this, EnterNameActivity.class);
315            startActivity(intent);
316            finish();
317        }
318    }
319
320    @Override
321    public void onPrimaryClipChanged() {
322        this.pasted = null;
323        if (pinEntryWrapper.isEmpty()) {
324            pastePinFromClipboard();
325        }
326    }
327
328    @Override
329    public void onVerificationFailed(final int code) {
330        runOnUiThread(
331                () -> {
332                    setVerifyingState(false);
333                    if (code == 401 || code == 404) {
334                        AlertDialog.Builder builder = new AlertDialog.Builder(this);
335                        builder.setMessage(
336                                code == 404 ? R.string.pin_expired : R.string.incorrect_pin);
337                        builder.setPositiveButton(R.string.ok, null);
338                        builder.create().show();
339                    } else {
340                        ApiDialogHelper.createError(this, code).show();
341                    }
342                });
343    }
344
345    @Override
346    public void onVerificationSucceeded() {
347        runOnUiThread(this::performPostVerificationRedirect);
348    }
349
350    @Override
351    public void onVerificationRetryAt(long timestamp) {
352        this.retryVerificationAfter = timestamp;
353        runOnUiThread(
354                () -> {
355                    ApiDialogHelper.createTooManyAttempts(this).show();
356                    setVerifyingState(false);
357                });
358        mHandler.removeCallbacks(VERIFICATION_TIMEOUT_UPDATER);
359        runOnUiThread(VERIFICATION_TIMEOUT_UPDATER);
360    }
361
362    @Override
363    public void startBackgroundVerification(String pin) {
364        pinEntryWrapper.setPin(pin);
365        setVerifyingState(true);
366    }
367
368    // send sms again button callback
369    @Override
370    public void onVerificationRequestFailed(int code) {
371        runOnUiThread(
372                () -> {
373                    setRequestingVerificationState(false);
374                    ApiDialogHelper.createError(this, code).show();
375                });
376    }
377
378    // send sms again button callback
379    @Override
380    public void onVerificationRequested() {
381        runOnUiThread(
382                () -> {
383                    pinEntryWrapper.clear();
384                    setRequestingVerificationState(false);
385                    AlertDialog.Builder builder = new AlertDialog.Builder(this);
386                    builder.setMessage(R.string.we_have_sent_you_another_sms);
387                    builder.setPositiveButton(R.string.ok, null);
388                    builder.create().show();
389                });
390    }
391
392    @Override
393    public void onVerificationRequestedRetryAt(long timestamp) {
394        this.retrySmsAfter = timestamp;
395        runOnUiThread(
396                () -> {
397                    ApiDialogHelper.createRateLimited(this, timestamp).show();
398                    setRequestingVerificationState(false);
399                });
400        mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER);
401        runOnUiThread(SMS_TIMEOUT_UPDATER);
402    }
403}