create api calls

This commit is contained in:
Daniel Gultsch 2018-10-22 20:08:05 +02:00
parent 31eb89e2fb
commit 2fa629d113
7 changed files with 319 additions and 26 deletions

View file

@ -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);
}
}

View file

@ -760,9 +760,11 @@
<string name="please_enter_your_phone_number">Please enter your phone number.</string>
<string name="search_countries">Search countries</string>
<string name="verify_x">Verify %s</string>
<string name="we_have_sent_you_an_sms"><![CDATA[We have sent you an SMS to <b>%s</b>.]]></string>
<string name="we_have_sent_you_an_sms_to_x"><![CDATA[We have sent you an SMS to <b>%s</b>.]]></string>
<string name="we_have_sent_you_the_sms_again">We have sent you the SMS again.</string>
<string name="please_enter_pin_below">Please enter the 6 digit pin below.</string>
<string name="resend_sms">Resend SMS</string>
<string name="resend_sms_in">Resend SMS (%s)</string>
<string name="back">back</string>
<string name="possible_pin">Automatically pasted possible pin from clipboard.</string>
<string name="please_enter_pin">Please enter your 6 digit pin.</string>
@ -770,4 +772,5 @@
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="verifying">Verifying…</string>
<string name="requesting_sms">Requesting SMS…</string>
</resources>

View file

@ -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<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
private final Set<OnVerification> 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);
}
}

View file

@ -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();
});
}

View file

@ -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);
}
}

View file

@ -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)));
}

View file

@ -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" />
<TextView