diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index 40a551515..7b774b681 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -24,7 +24,11 @@ public class Plain extends SaslMechanism { @Override public String getClientFirstMessage() { - final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword(); - return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); + return getMessage(account.getUsername(), account.getPassword()); + } + + public static String getMessage(String username, String password) { + final String message = '\u0000' + username + '\u0000' + password; + return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f401c0f45..b006e3478 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -760,9 +760,11 @@ Please enter your phone number. Search countries Verify %s - %s.]]> + %s.]]> + We have sent you the SMS again. Please enter the 6 digit pin below. Resend SMS + Resend SMS (%s) back Automatically pasted possible pin from clipboard. Please enter your 6 digit pin. @@ -770,4 +772,5 @@ Yes No Verifying… + Requesting SMS… diff --git a/src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java index 7b73df8d9..1ce9a78fb 100644 --- a/src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -1,8 +1,17 @@ package eu.siacs.conversations.services; +import android.os.SystemClock; import android.util.Log; +import java.io.BufferedWriter; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; import java.security.SecureRandom; import java.util.Collections; import java.util.Set; @@ -10,7 +19,9 @@ import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.sasl.Plain; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import io.michaelrocks.libphonenumber.android.Phonenumber; @@ -18,12 +29,20 @@ import rocks.xmpp.addr.Jid; public class QuickConversationsService { + + public static final int API_ERROR_OTHER = -1; + public static final int API_ERROR_UNKNOWN_HOST = -2; + public static final int API_ERROR_CONNECT = -3; + + private static final String BASE_URL = "https://venus.fritz.box:4567"; + private final XmppConnectionService service; private final Set mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>()); private final Set mOnVerification = Collections.newSetFromMap(new WeakHashMap<>()); private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false); + private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false); QuickConversationsService(XmppConnectionService xmppConnectionService) { this.service = xmppConnectionService; @@ -54,31 +73,136 @@ public class QuickConversationsService { } public void requestVerification(Phonenumber.PhoneNumber phoneNumber) { + final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber); + + /** + * GET /authentication/+phoneNumber + * + * - returns too many requests, (sms ist unterwegs), retry after seconden -- auf jeden fall in nächste activity (verify activity) weiter leiten weil es sein kann das sms noch ankommt + * - returns OK; success (auch in activity weiter lassen. aber ohne error paramater; dh send again button is activ; und vielleicht kurzer toast bzw snackbar + * - returns invalid request user error wenn die phone number falsch ist + */ + if (mVerificationRequestInProgress.compareAndSet(false, true)) { + new Thread(() -> { + try { + + Thread.sleep(5000); + + final URL url = new URL(BASE_URL + "/authentication/" + e164); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + final int code = connection.getResponseCode(); + if (code == 200) { + createAccountAndWait(phoneNumber, 0L); + } else if (code == 429) { + createAccountAndWait(phoneNumber, retryAfter(connection)); + } else { + synchronized (mOnVerificationRequested) { + for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) { + onVerificationRequested.onVerificationRequestFailed(code); + } + } + } + } catch (Exception e) { + final int code = getApiErrorCode(e); + synchronized (mOnVerificationRequested) { + for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) { + onVerificationRequested.onVerificationRequestFailed(code); + } + } + } finally { + mVerificationRequestInProgress.set(false); + } + }).start(); + } + + + } + + private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) { String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber); - Log.d(Config.LOGTAG,"requesting verification for "+PhoneNumberUtilWrapper.normalize(service,phoneNumber)); - Account account = new Account(Jid.of(local,"quick.conversations.im",null), CryptoHelper.createPassword(new SecureRandom())); - account.setOption(Account.OPTION_DISABLED, true); - account.setOption(Account.OPTION_UNVERIFIED, true); - service.createAccount(account); + Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber)); + Jid jid = Jid.of(local, "quick.conversations.im", null); + Account account = AccountUtils.getFirst(service); + if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) { + if (account != null) { + service.deleteAccount(account); + } + account = new Account(jid, CryptoHelper.createPassword(new SecureRandom())); + account.setOption(Account.OPTION_DISABLED, true); + account.setOption(Account.OPTION_UNVERIFIED, true); + service.createAccount(account); + } synchronized (mOnVerificationRequested) { - for(OnVerificationRequested onVerificationRequested : mOnVerificationRequested) { - onVerificationRequested.onVerificationRequested(); + for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) { + if (timestamp <= 0) { + onVerificationRequested.onVerificationRequested(); + } else { + onVerificationRequested.onVerificationRequestedRetryAt(timestamp); + } } } } public void verify(Account account, String pin) { + /** + * POST /password + * authentication gesetzt mit telephone nummber und verification code + * body = password + * + * retry after, too many requests + * code wrong + * OK (weiterleiten auf publish avatar activity + * + */ if (mVerificationInProgress.compareAndSet(false, true)) { new Thread(() -> { try { + Thread.sleep(5000); - synchronized (mOnVerification) { - for (OnVerification onVerification : mOnVerification) { - onVerification.onVerificationFailed(); + + final URL url = new URL(BASE_URL + "/password"); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin)); + final OutputStream os = connection.getOutputStream(); + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); + writer.write(account.getPassword()); + writer.flush(); + writer.close(); + os.close(); + connection.connect(); + final int code = connection.getResponseCode(); + if (code == 200) { + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationSucceeded(); + } + } + } else if (code == 429) { + final long retryAfter = retryAfter(connection); + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationRetryAt(retryAfter); + } + } + } else { + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationFailed(code); + } + } + } + } catch (Exception e) { + final int code = getApiErrorCode(e); + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationFailed(code); } } - } catch (InterruptedException e) { - e.printStackTrace(); } finally { mVerificationInProgress.set(false); } @@ -86,17 +210,46 @@ public class QuickConversationsService { } } + private static int getApiErrorCode(Exception e) { + if (e instanceof UnknownHostException) { + return API_ERROR_UNKNOWN_HOST; + } else if (e instanceof ConnectException) { + return API_ERROR_CONNECT; + } else { + Log.d(Config.LOGTAG,e.getClass().getName()); + return API_ERROR_OTHER; + } + } + + private static long retryAfter(HttpURLConnection connection) { + try { + return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L); + } catch (Exception e) { + return 0; + } + } + public boolean isVerifying() { return mVerificationInProgress.get(); } + public boolean isRequestingVerification() { + return mVerificationRequestInProgress.get(); + } + public interface OnVerificationRequested { void onVerificationRequestFailed(int code); + void onVerificationRequested(); + + void onVerificationRequestedRetryAt(long timestamp); } public interface OnVerification { - void onVerificationFailed(); + void onVerificationFailed(int code); + void onVerificationSucceeded(); + + void onVerificationRetryAt(long timestamp); } } \ No newline at end of file diff --git a/src/quick/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java b/src/quick/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java index eae5c7315..24670a31b 100644 --- a/src/quick/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java +++ b/src/quick/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java @@ -30,7 +30,10 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve private static final int REQUEST_CHOOSE_COUNTRY = 0x1234; private ActivityEnterNumberBinding binding; + private String region = null; + private boolean requestingVerification = false; + private final TextWatcher countryCodeTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { @@ -78,6 +81,7 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve super.onCreate(savedInstanceState); String region = savedInstanceState != null ? savedInstanceState.getString("region") : null; + boolean requestingVerification = savedInstanceState != null && savedInstanceState.getBoolean("requesting_verification", false); if (region != null) { this.region = region; } else { @@ -91,6 +95,7 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve setSupportActionBar((Toolbar) this.binding.toolbar); this.binding.countryCode.addTextChangedListener(this.countryCodeTextWatcher); this.binding.countryCode.setText(String.valueOf(PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(this.region))); + setRequestingVerificationState(requestingVerification); } @Override @@ -98,6 +103,7 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve if (this.region != null) { savedInstanceState.putString("region", this.region); } + savedInstanceState.putBoolean("requesting_verification", this.requestingVerification); super.onSaveInstanceState(savedInstanceState); } @@ -142,9 +148,19 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve } private void onPhoneNumberEntered(Phonenumber.PhoneNumber phoneNumber) { + setRequestingVerificationState(true); xmppConnectionService.getQuickConversationsService().requestVerification(phoneNumber); } + private void setRequestingVerificationState(boolean requesting) { + this.requestingVerification = requesting; + this.binding.countryCode.setEnabled(!requesting); + this.binding.country.setEnabled(!requesting); + this.binding.number.setEnabled(!requesting); + this.binding.next.setEnabled(!requesting); + this.binding.next.setText(requesting ? R.string.requesting_sms : R.string.next); + } + @Override public void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -160,13 +176,25 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve @Override public void onVerificationRequestFailed(int code) { - + runOnUiThread(()->{ + setRequestingVerificationState(false); + }); } @Override public void onVerificationRequested() { runOnUiThread(() -> { - startActivity(new Intent(this,VerifyActivity.class)); + startActivity(new Intent(this, VerifyActivity.class)); + finish(); + }); + } + + @Override + public void onVerificationRequestedRetryAt(long timestamp) { + runOnUiThread(() -> { + Intent intent = new Intent(this, VerifyActivity.class); + intent.putExtra(VerifyActivity.EXTRA_RETRY_SMS_AFTER, timestamp); + startActivity(intent); finish(); }); } diff --git a/src/quick/java/eu/siacs/conversations/ui/VerifyActivity.java b/src/quick/java/eu/siacs/conversations/ui/VerifyActivity.java index f1425e272..edecb52ae 100644 --- a/src/quick/java/eu/siacs/conversations/ui/VerifyActivity.java +++ b/src/quick/java/eu/siacs/conversations/ui/VerifyActivity.java @@ -8,11 +8,15 @@ import android.content.Context; import android.content.Intent; import android.databinding.DataBindingUtil; import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; import android.support.design.widget.Snackbar; import android.support.v7.widget.Toolbar; import android.text.Html; +import android.util.Log; import android.view.View; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityVerifyBinding; import eu.siacs.conversations.entities.Account; @@ -20,10 +24,14 @@ import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.ui.util.PinEntryWrapper; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; +import eu.siacs.conversations.utils.TimeframeUtils; +import io.michaelrocks.libphonenumber.android.NumberParseException; import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; -public class VerifyActivity extends XmppActivity implements ClipboardManager.OnPrimaryClipChangedListener, QuickConversationsService.OnVerification { +public class VerifyActivity extends XmppActivity implements ClipboardManager.OnPrimaryClipChangedListener, QuickConversationsService.OnVerification, QuickConversationsService.OnVerificationRequested { + + public static final String EXTRA_RETRY_SMS_AFTER = "retry_sms_after"; private ActivityVerifyBinding binding; private Account account; @@ -31,14 +39,43 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP private ClipboardManager clipboardManager; private String pasted = null; private boolean verifying = false; + private boolean requestingVerification = false; + private long retrySmsAfter = 0; + private final Handler mHandler = new Handler(); + + + private final Runnable SMS_TIMEOUT_UPDATER = new Runnable() { + @Override + public void run() { + if (setTimeoutLabelInResendButton()) { + mHandler.postDelayed(this,300); + } + } + }; + + private boolean setTimeoutLabelInResendButton() { + if (retrySmsAfter != 0) { + long remaining = retrySmsAfter - SystemClock.elapsedRealtime(); + if (remaining >= 0) { + binding.resendSms.setEnabled(false); + binding.resendSms.setText(getString(R.string.resend_sms_in, TimeframeUtils.resolve(VerifyActivity.this,remaining))); + return true; + } + } + binding.resendSms.setEnabled(true); + binding.resendSms.setText(R.string.resend_sms); + return false; + } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); String pin = savedInstanceState != null ? savedInstanceState.getString("pin") : null; boolean verifying = savedInstanceState != null && savedInstanceState.getBoolean("verifying"); + boolean requestingVerification = savedInstanceState != null && savedInstanceState.getBoolean("requesting_verification", false); this.pasted = savedInstanceState != null ? savedInstanceState.getString("pasted") : null; + this.retrySmsAfter = savedInstanceState != null ? savedInstanceState.getLong(EXTRA_RETRY_SMS_AFTER,0L) : 0L; this.binding = DataBindingUtil.setContentView(this, R.layout.activity_verify); setSupportActionBar((Toolbar) this.binding.toolbar); this.pinEntryWrapper = new PinEntryWrapper(binding.pinBox); @@ -47,8 +84,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP } binding.back.setOnClickListener(this::onBackButton); binding.next.setOnClickListener(this::onNextButton); + binding.resendSms.setOnClickListener(this::onResendSmsButton); clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); setVerifyingState(verifying); + setRequestingVerificationState(requestingVerification); } private void onBackButton(View view) { @@ -88,6 +127,15 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP } } + private void onResendSmsButton(View view) { + try { + xmppConnectionService.getQuickConversationsService().requestVerification(PhoneNumberUtilWrapper.toPhoneNumber(this, account.getJid())); + setRequestingVerificationState(true); + } catch (NumberParseException e) { + + } + } + private void setVerifyingState(boolean verifying) { this.verifying = verifying; this.binding.back.setText(verifying ? R.string.cancel : R.string.back); @@ -99,6 +147,17 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP this.binding.progressBar.setIndeterminate(verifying); } + private void setRequestingVerificationState(boolean requesting) { + this.requestingVerification = requesting; + if (requesting) { + this.binding.resendSms.setEnabled(false); + this.binding.resendSms.setText(R.string.requesting_sms); + } else { + setTimeoutLabelInResendButton(); + } + + } + @Override protected void refreshUiReal() { @@ -107,18 +166,22 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP @Override void onBackendConnected() { xmppConnectionService.getQuickConversationsService().addOnVerificationListener(this); + xmppConnectionService.getQuickConversationsService().addOnVerificationRequestedListener(this); this.account = AccountUtils.getFirst(xmppConnectionService); if (this.account == null) { return; } - this.binding.weHaveSent.setText(Html.fromHtml(getString(R.string.we_have_sent_you_an_sms, PhoneNumberUtilWrapper.prettyPhoneNumber(this, this.account.getJid())))); + this.binding.weHaveSent.setText(Html.fromHtml(getString(R.string.we_have_sent_you_an_sms_to_x, PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, this.account.getJid())))); setVerifyingState(xmppConnectionService.getQuickConversationsService().isVerifying()); + setRequestingVerificationState(xmppConnectionService.getQuickConversationsService().isRequestingVerification()); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putString("pin", this.pinEntryWrapper.getPin()); savedInstanceState.putBoolean("verifying", this.verifying); + savedInstanceState.putBoolean("requesting_verification", this.requestingVerification); + savedInstanceState.putLong(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter); if (this.pasted != null) { savedInstanceState.putString("pasted", this.pasted); } @@ -129,14 +192,19 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP public void onStart() { super.onStart(); clipboardManager.addPrimaryClipChangedListener(this); + if (this.retrySmsAfter > 0) { + mHandler.post(SMS_TIMEOUT_UPDATER); + } } @Override public void onStop() { super.onStop(); + mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER); clipboardManager.removePrimaryClipChangedListener(this); if (xmppConnectionService != null) { xmppConnectionService.getQuickConversationsService().removeOnVerificationListener(this); + xmppConnectionService.getQuickConversationsService().removeOnVerificationRequestedListener(this); } } @@ -174,14 +242,49 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP } @Override - public void onVerificationFailed() { + public void onVerificationFailed(int code) { runOnUiThread(() -> { setVerifyingState(false); }); + Log.d(Config.LOGTAG,"code="+code); } @Override public void onVerificationSucceeded() { } + + @Override + public void onVerificationRetryAt(long timestamp) { + + } + + //send sms again button callback + @Override + public void onVerificationRequestFailed(int code) { + runOnUiThread(()->{ + setRequestingVerificationState(false); + }); + Log.d(Config.LOGTAG,"code="+code); + } + + //send sms again button callback + @Override + public void onVerificationRequested() { + runOnUiThread(()-> { + setRequestingVerificationState(false); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.we_have_sent_you_the_sms_again); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + }); + } + + @Override + public void onVerificationRequestedRetryAt(long timestamp) { + this.retrySmsAfter = timestamp; + runOnUiThread(()-> setRequestingVerificationState(false)); + mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER); + runOnUiThread(SMS_TIMEOUT_UPDATER); + } } diff --git a/src/quick/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java b/src/quick/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java index 650ba7d0f..bccad767f 100644 --- a/src/quick/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java +++ b/src/quick/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java @@ -41,16 +41,18 @@ public class PhoneNumberUtilWrapper { return locale.getCountry(); } - public static String prettyPhoneNumber(Context context, Jid jid) { - PhoneNumberUtil phoneNumberUtil = getInstance(context); + public static String toFormattedPhoneNumber(Context context, Jid jid) { try { - Phonenumber.PhoneNumber phoneNumber = phoneNumberUtil.parse(jid.getEscapedLocal(), "de"); - return phoneNumberUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); + return getInstance(context).format(toPhoneNumber(context, jid), PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); } catch (Exception e) { return jid.getEscapedLocal(); } } + public static Phonenumber.PhoneNumber toPhoneNumber(Context context, Jid jid) throws NumberParseException { + return getInstance(context).parse(jid.getEscapedLocal(), "de"); + } + public static String normalize(Context context, String number) throws NumberParseException { return normalize(context, getInstance(context).parse(number, getUserCountry(context))); } diff --git a/src/quick/res/layout/activity_verify.xml b/src/quick/res/layout/activity_verify.xml index 4d44883a0..6d9d13ef4 100644 --- a/src/quick/res/layout/activity_verify.xml +++ b/src/quick/res/layout/activity_verify.xml @@ -39,7 +39,7 @@ android:id="@+id/we_have_sent" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/we_have_sent_you_an_sms" + android:text="@string/we_have_sent_you_an_sms_to_x" android:textAppearance="@style/TextAppearance.Conversations.Subhead" />