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}