Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/iNPUTmice/Conversations.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Gultsch <daniel@gultsch.de>2018-10-28 14:34:17 +0300
committerDaniel Gultsch <daniel@gultsch.de>2018-10-31 15:33:55 +0300
commit87cc53b8b5fcad666a981676f522ba07d28c805c (patch)
tree7a035dd3c7c13694b90d422afe5ac358fb23402d /src/quicksy/java
parenta49a5790c79c1a9d59e1934159d0d67c326d1322 (diff)
renamed build flavors
Diffstat (limited to 'src/quicksy/java')
-rw-r--r--src/quicksy/java/eu/siacs/conversations/android/PhoneNumberContact.java68
-rw-r--r--src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java308
-rw-r--r--src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java129
-rw-r--r--src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java220
-rw-r--r--src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java341
-rw-r--r--src/quicksy/java/eu/siacs/conversations/ui/adapter/CountryAdapter.java70
-rw-r--r--src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java240
-rw-r--r--src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java83
-rw-r--r--src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java149
-rw-r--r--src/quicksy/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java116
-rw-r--r--src/quicksy/java/eu/siacs/conversations/utils/SignupUtils.java37
11 files changed, 1761 insertions, 0 deletions
diff --git a/src/quicksy/java/eu/siacs/conversations/android/PhoneNumberContact.java b/src/quicksy/java/eu/siacs/conversations/android/PhoneNumberContact.java
new file mode 100644
index 000000000..e044746a7
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/android/PhoneNumberContact.java
@@ -0,0 +1,68 @@
+package eu.siacs.conversations.android;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Build;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+import io.michaelrocks.libphonenumber.android.NumberParseException;
+
+public class PhoneNumberContact extends AbstractPhoneContact {
+
+ private String phoneNumber;
+
+ public String getPhoneNumber() {
+ return phoneNumber;
+ }
+
+ private PhoneNumberContact(Context context, Cursor cursor) throws IllegalArgumentException {
+ super(cursor);
+ try {
+ this.phoneNumber = PhoneNumberUtilWrapper.normalize(context,cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
+ } catch (NumberParseException | NullPointerException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static Map<String, PhoneNumberContact> load(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ return Collections.emptyMap();
+ }
+ final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
+ ContactsContract.Data.DISPLAY_NAME,
+ ContactsContract.Data.PHOTO_URI,
+ ContactsContract.Data.LOOKUP_KEY,
+ ContactsContract.CommonDataKinds.Phone.NUMBER};
+ final Cursor cursor;
+ try {
+ cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null);
+ } catch (Exception e) {
+ return Collections.emptyMap();
+ }
+ final HashMap<String, PhoneNumberContact> contacts = new HashMap<>();
+ while (cursor != null && cursor.moveToNext()) {
+ try {
+ final PhoneNumberContact contact = new PhoneNumberContact(context, cursor);
+ final PhoneNumberContact preexisting = contacts.get(contact.getPhoneNumber());
+ if (preexisting == null || preexisting.rating() < contact.rating()) {
+ contacts.put(contact.getPhoneNumber(), contact);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.d(Config.LOGTAG, "unable to create phone contact");
+ }
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ return contacts;
+ }
+}
diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java
new file mode 100644
index 000000000..362bace72
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java
@@ -0,0 +1,308 @@
+package eu.siacs.conversations.services;
+
+
+import android.content.SharedPreferences;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+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.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.PhoneNumberContact;
+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 eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import io.michaelrocks.libphonenumber.android.Phonenumber;
+import rocks.xmpp.addr.Jid;
+
+public class QuickConversationsService extends AbstractQuickConversationsService {
+
+
+ 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;
+ public static final int API_ERROR_SSL_HANDSHAKE = -4;
+ public static final int API_ERROR_AIRPLANE_MODE = -5;
+
+ private static final String BASE_URL = "http://venus.fritz.box:4567";
+
+ private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
+
+ 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) {
+ super(xmppConnectionService);
+ }
+
+ public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
+ synchronized (mOnVerificationRequested) {
+ mOnVerificationRequested.add(onVerificationRequested);
+ }
+ }
+
+ public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
+ synchronized (mOnVerificationRequested) {
+ mOnVerificationRequested.remove(onVerificationRequested);
+ }
+ }
+
+ public void addOnVerificationListener(OnVerification onVerification) {
+ synchronized (mOnVerification) {
+ mOnVerification.add(onVerification);
+ }
+ }
+
+ public void removeOnVerificationListener(OnVerification onVerification) {
+ synchronized (mOnVerification) {
+ mOnVerification.remove(onVerification);
+ }
+ }
+
+ public void requestVerification(Phonenumber.PhoneNumber phoneNumber) {
+ final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
+ 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);
+ setHeader(connection);
+ 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));
+ Jid jid = Jid.of(local, Config.QUICKSY_DOMAIN, 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) {
+ if (timestamp <= 0) {
+ onVerificationRequested.onVerificationRequested();
+ } else {
+ onVerificationRequested.onVerificationRequestedRetryAt(timestamp);
+ }
+ }
+ }
+ }
+
+ public void verify(final Account account, String pin) {
+ if (mVerificationInProgress.compareAndSet(false, true)) {
+ new Thread(() -> {
+ try {
+
+ Thread.sleep(5000);
+
+ 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));
+ setHeader(connection);
+ 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) {
+ account.setOption(Account.OPTION_UNVERIFIED, false);
+ account.setOption(Account.OPTION_DISABLED, false);
+ service.updateAccount(account);
+ 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);
+ }
+ }
+ } finally {
+ mVerificationInProgress.set(false);
+ }
+ }).start();
+ }
+ }
+
+ private void setHeader(HttpURLConnection connection) {
+ connection.setRequestProperty("User-Agent", service.getIqGenerator().getUserAgent());
+ connection.setRequestProperty("Installation-Id", getInstallationId());
+ connection.setRequestProperty("Accept-Language", Locale.getDefault().getLanguage());
+ }
+
+ private String getInstallationId() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
+ String id = preferences.getString(INSTALLATION_ID, null);
+ if (id != null) {
+ return id;
+ } else {
+ id = UUID.randomUUID().toString();
+ preferences.edit().putString(INSTALLATION_ID, id).apply();
+ return id;
+ }
+
+ }
+
+ private int getApiErrorCode(Exception e) {
+ if (!service.hasInternetConnection()) {
+ return API_ERROR_AIRPLANE_MODE;
+ } else if (e instanceof UnknownHostException) {
+ return API_ERROR_UNKNOWN_HOST;
+ } else if (e instanceof ConnectException) {
+ return API_ERROR_CONNECT;
+ } else if (e instanceof SSLHandshakeException) {
+ return API_ERROR_SSL_HANDSHAKE;
+ } 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();
+ }
+
+ @Override
+ public void considerSync() {
+ Map<String, PhoneNumberContact> contacts = PhoneNumberContact.load(service);
+ for(Account account : service.getAccounts()) {
+ considerSync(account, contacts);
+ }
+ }
+
+ private void considerSync(Account account, Map<String, PhoneNumberContact> contacts) {
+ XmppConnection xmppConnection = account.getXmppConnection();
+ Jid syncServer = xmppConnection == null ? null : xmppConnection.findDiscoItemByFeature(Namespace.SYNCHRONIZATION);
+ if (syncServer == null) {
+ Log.d(Config.LOGTAG,account.getJid().asBareJid()+": skipping sync. no sync server found");
+ return;
+ }
+ Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sending phone list to "+syncServer);
+ List<Element> entries = new ArrayList<>();
+ for(PhoneNumberContact c : contacts.values()) {
+ entries.add(new Element("entry").setAttribute("number",c.getPhoneNumber()));
+ }
+ Element phoneBook = new Element("phone-book",Namespace.SYNCHRONIZATION);
+ phoneBook.setChildren(entries);
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
+ iqPacket.setTo(syncServer);
+ iqPacket.addChild(phoneBook);
+ service.sendIqPacket(account, iqPacket, null);
+ }
+
+ public interface OnVerificationRequested {
+ void onVerificationRequestFailed(int code);
+
+ void onVerificationRequested();
+
+ void onVerificationRequestedRetryAt(long timestamp);
+ }
+
+ public interface OnVerification {
+ void onVerificationFailed(int code);
+
+ void onVerificationSucceeded();
+
+ void onVerificationRetryAt(long timestamp);
+ }
+} \ No newline at end of file
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java
new file mode 100644
index 000000000..c17776554
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java
@@ -0,0 +1,129 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.databinding.DataBindingUtil;
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.Toolbar;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityChooseCountryBinding;
+import eu.siacs.conversations.ui.adapter.CountryAdapter;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+
+public class ChooseCountryActivity extends ActionBarActivity implements CountryAdapter.OnCountryClicked {
+
+ private ActivityChooseCountryBinding binding;
+
+ private List<PhoneNumberUtilWrapper.Country> countries = new ArrayList<>();
+ private CountryAdapter countryAdapter = new CountryAdapter(countries);
+ private final TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(final Editable editable) {
+ filterCountries(editable.toString());
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
+ }
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+ }
+ };
+ private EditText mSearchEditText;
+ private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
+
+ @Override
+ public boolean onMenuItemActionExpand(final MenuItem item) {
+ mSearchEditText.post(() -> {
+ mSearchEditText.requestFocus();
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(final MenuItem item) {
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
+ mSearchEditText.setText("");
+ filterCountries(null);
+ return true;
+ }
+ };
+ private TextView.OnEditorActionListener mSearchDone = (v, actionId, event) -> {
+ if (countries.size() == 1) {
+ onCountryClicked(countries.get(0));
+ }
+ return true;
+ };
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_choose_country);
+ setSupportActionBar((Toolbar) this.binding.toolbar);
+ configureActionBar(getSupportActionBar());
+ this.countries.addAll(PhoneNumberUtilWrapper.getCountries(this));
+ Collections.sort(this.countries);
+ this.binding.countries.setAdapter(countryAdapter);
+ this.binding.countries.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+ countryAdapter.setOnCountryClicked(this);
+ countryAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onCountryClicked(PhoneNumberUtilWrapper.Country country) {
+ Intent data = new Intent();
+ data.putExtra("region", country.getRegion());
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.choose_country, menu);
+ final MenuItem menuSearchView = menu.findItem(R.id.action_search);
+ final View mSearchView = menuSearchView.getActionView();
+ mSearchEditText = mSearchView.findViewById(R.id.search_field);
+ mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+ mSearchEditText.setHint(R.string.search_countries);
+ mSearchEditText.setOnEditorActionListener(mSearchDone);
+ menuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+ return true;
+ }
+
+ private void filterCountries(String needle) {
+ List<PhoneNumberUtilWrapper.Country> countries = PhoneNumberUtilWrapper.getCountries(this);
+ Iterator<PhoneNumberUtilWrapper.Country> iterator = countries.iterator();
+ while(iterator.hasNext()) {
+ final PhoneNumberUtilWrapper.Country country = iterator.next();
+ if(needle != null && !country.getName().toLowerCase(Locale.getDefault()).contains(needle.toLowerCase(Locale.getDefault()))) {
+ iterator.remove();
+ }
+ }
+ this.countries.clear();
+ this.countries.addAll(countries);
+ this.countryAdapter.notifyDataSetChanged();
+ }
+
+}
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java
new file mode 100644
index 000000000..200ca9e12
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java
@@ -0,0 +1,220 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.databinding.DataBindingUtil;
+import android.os.Bundle;
+import android.support.v7.widget.Toolbar;
+import android.text.Editable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.EditText;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityEnterNumberBinding;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.ui.drawable.TextDrawable;
+import eu.siacs.conversations.ui.util.ApiDialogHelper;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+import io.michaelrocks.libphonenumber.android.NumberParseException;
+import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
+import io.michaelrocks.libphonenumber.android.Phonenumber;
+
+public class EnterPhoneNumberActivity extends XmppActivity implements QuickConversationsService.OnVerificationRequested {
+
+ private static final int REQUEST_CHOOSE_COUNTRY = 0x1234;
+
+ private ActivityEnterNumberBinding binding;
+
+ private String region = null;
+ private final TextWatcher countryCodeTextWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ final String text = editable.toString();
+ try {
+ final int oldCode = region != null ? PhoneNumberUtilWrapper.getInstance(EnterPhoneNumberActivity.this).getCountryCodeForRegion(region) : 0;
+ final int code = Integer.parseInt(text);
+ if (oldCode != code) {
+ region = PhoneNumberUtilWrapper.getInstance(EnterPhoneNumberActivity.this).getRegionCodeForCountryCode(code);
+ }
+ if ("ZZ".equals(region)) {
+ binding.country.setText(TextUtils.isEmpty(text) ? R.string.choose_a_country : R.string.invalid_country_code);
+ } else {
+ binding.number.requestFocus();
+ binding.country.setText(PhoneNumberUtilWrapper.getCountryForCode(region));
+ }
+ } catch (NumberFormatException e) {
+ binding.country.setText(TextUtils.isEmpty(text) ? R.string.choose_a_country : R.string.invalid_country_code);
+ }
+ }
+ };
+ private boolean requestingVerification = false;
+
+ @Override
+ protected void refreshUiReal() {
+
+ }
+
+ @Override
+ void onBackendConnected() {
+ xmppConnectionService.getQuickConversationsService().addOnVerificationRequestedListener(this);
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ 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 {
+ this.region = PhoneNumberUtilWrapper.getUserCountry(this);
+ }
+
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_number);
+ this.binding.countryCode.setCompoundDrawables(new TextDrawable(this.binding.countryCode, "+"), null, null, null);
+ this.binding.country.setOnClickListener(this::onSelectCountryClick);
+ this.binding.next.setOnClickListener(this::onNextClick);
+ setSupportActionBar((Toolbar) this.binding.toolbar);
+ this.binding.countryCode.addTextChangedListener(this.countryCodeTextWatcher);
+ this.binding.countryCode.setText(String.valueOf(PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(this.region)));
+ this.binding.number.setOnKeyListener((v, keyCode, event) -> {
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return false;
+ }
+ final EditText editText = (EditText) v;
+ final boolean cursorAtZero = editText.getSelectionEnd() == 0 && editText.getSelectionStart() == 0;
+ if (keyCode == KeyEvent.KEYCODE_DEL && (cursorAtZero || editText.getText().length() == 0)) {
+ final Editable countryCode = this.binding.countryCode.getText();
+ if (countryCode.length() > 0) {
+ countryCode.delete(countryCode.length() - 1, countryCode.length());
+ this.binding.countryCode.setSelection(countryCode.length());
+ }
+ this.binding.countryCode.requestFocus();
+ return true;
+ }
+ return false;
+ });
+ setRequestingVerificationState(requestingVerification);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ if (this.region != null) {
+ savedInstanceState.putString("region", this.region);
+ }
+ savedInstanceState.putBoolean("requesting_verification", this.requestingVerification);
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onStop() {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.getQuickConversationsService().removeOnVerificationRequestedListener(this);
+ }
+ super.onStop();
+ }
+
+ private void onNextClick(View v) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ try {
+ final Editable number = this.binding.number.getText();
+ final String input = number.toString();
+ final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtilWrapper.getInstance(this).parse(input, region);
+ this.binding.countryCode.setText(String.valueOf(phoneNumber.getCountryCode()));
+ number.clear();
+ number.append(String.valueOf(phoneNumber.getNationalNumber()));
+ final String formattedPhoneNumber = PhoneNumberUtilWrapper.getInstance(this).format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL);
+
+ if (PhoneNumberUtilWrapper.getInstance(this).isValidNumber(phoneNumber)) {
+ builder.setMessage(Html.fromHtml(getString(R.string.we_will_be_verifying, formattedPhoneNumber)));
+ builder.setNegativeButton(R.string.edit, null);
+ builder.setPositiveButton(R.string.ok, (dialog, which) -> onPhoneNumberEntered(phoneNumber));
+ } else {
+ builder.setMessage(getString(R.string.not_a_valid_phone_number, formattedPhoneNumber));
+ builder.setPositiveButton(R.string.ok, null);
+ }
+ Log.d(Config.LOGTAG, phoneNumber.toString());
+ } catch (NumberParseException e) {
+ builder.setMessage(R.string.please_enter_your_phone_number);
+ builder.setPositiveButton(R.string.ok, null);
+ }
+ builder.create().show();
+ }
+
+ private void onSelectCountryClick(View view) {
+ Intent intent = new Intent(this, ChooseCountryActivity.class);
+ startActivityForResult(intent, REQUEST_CHOOSE_COUNTRY);
+ }
+
+ 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);
+ this.binding.progressBar.setVisibility(requesting ? View.VISIBLE : View.GONE);
+ this.binding.progressBar.setIndeterminate(requesting);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK && requestCode == REQUEST_CHOOSE_COUNTRY) {
+ String region = data.getStringExtra("region");
+ if (region != null) {
+ this.region = region;
+ final int countryCode = PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(region);
+ this.binding.countryCode.setText(String.valueOf(countryCode));
+ }
+ }
+ }
+
+ @Override
+ public void onVerificationRequestFailed(int code) {
+ runOnUiThread(() -> {
+ setRequestingVerificationState(false);
+ ApiDialogHelper.createError(this, code).show();
+ });
+ }
+
+ @Override
+ public void onVerificationRequested() {
+ runOnUiThread(() -> {
+ 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/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java
new file mode 100644
index 000000000..b34574492
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java
@@ -0,0 +1,341 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+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.view.View;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityVerifyBinding;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.ui.util.ApiDialogHelper;
+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, QuickConversationsService.OnVerificationRequested {
+
+ public static final String EXTRA_RETRY_SMS_AFTER = "retry_sms_after";
+ private static final String EXTRA_RETRY_VERIFICATION_AFTER = "retry_verification_after";
+ private final Handler mHandler = new Handler();
+ private ActivityVerifyBinding binding;
+ private Account account;
+ private PinEntryWrapper pinEntryWrapper;
+ private ClipboardManager clipboardManager;
+ private String pasted = null;
+ private boolean verifying = false;
+ private boolean requestingVerification = false;
+ private long retrySmsAfter = 0;
+ private final Runnable SMS_TIMEOUT_UPDATER = new Runnable() {
+ @Override
+ public void run() {
+ if (setTimeoutLabelInResendButton()) {
+ mHandler.postDelayed(this, 300);
+ }
+ }
+ };
+ private long retryVerificationAfter = 0;
+ private final Runnable VERIFICATION_TIMEOUT_UPDATER = new Runnable() {
+ @Override
+ public void run() {
+ if (setTimeoutLabelInNextButton()) {
+ 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;
+ }
+
+ private boolean setTimeoutLabelInNextButton() {
+ if (retryVerificationAfter != 0) {
+ long remaining = retryVerificationAfter - SystemClock.elapsedRealtime();
+ if (remaining >= 0) {
+ binding.next.setEnabled(false);
+ binding.next.setText(getString(R.string.wait_x, TimeframeUtils.resolve(VerifyActivity.this, remaining)));
+ return true;
+ }
+ }
+ this.binding.next.setEnabled(!verifying);
+ this.binding.next.setText(verifying ? R.string.verifying : R.string.next);
+ 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.retryVerificationAfter = savedInstanceState != null ? savedInstanceState.getLong(EXTRA_RETRY_VERIFICATION_AFTER, 0L) : 0L;
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_verify);
+ setSupportActionBar((Toolbar) this.binding.toolbar);
+ this.pinEntryWrapper = new PinEntryWrapper(binding.pinBox);
+ if (pin != null) {
+ this.pinEntryWrapper.setPin(pin);
+ }
+ 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) {
+ if (this.verifying) {
+ setVerifyingState(false);
+ return;
+ }
+ final Intent intent = new Intent(this, EnterPhoneNumberActivity.class);
+ if (this.account != null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.abort_registration_procedure);
+ builder.setPositiveButton(R.string.yes, (dialog, which) -> {
+ xmppConnectionService.deleteAccount(account);
+ startActivity(intent);
+ finish();
+ });
+ builder.setNegativeButton(R.string.no, null);
+ builder.create().show();
+ } else {
+ startActivity(intent);
+ finish();
+ }
+ }
+
+ private void onNextButton(View view) {
+ final String pin = pinEntryWrapper.getPin();
+ if (PinEntryWrapper.isValidPin(pin)) {
+ if (account != null && xmppConnectionService != null) {
+ setVerifyingState(true);
+ xmppConnectionService.getQuickConversationsService().verify(account, pin);
+ }
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.please_enter_pin);
+ builder.setPositiveButton(R.string.ok, null);
+ builder.create().show();
+ }
+ }
+
+ 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);
+ this.binding.next.setEnabled(!verifying);
+ this.binding.next.setText(verifying ? R.string.verifying : R.string.next);
+ this.binding.resendSms.setVisibility(verifying ? View.GONE : View.VISIBLE);
+ pinEntryWrapper.setEnabled(!verifying);
+ this.binding.progressBar.setVisibility(verifying ? View.VISIBLE : View.GONE);
+ 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() {
+
+ }
+
+ @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_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);
+ savedInstanceState.putLong(EXTRA_RETRY_VERIFICATION_AFTER, this.retryVerificationAfter);
+ if (this.pasted != null) {
+ savedInstanceState.putString("pasted", this.pasted);
+ }
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ clipboardManager.addPrimaryClipChangedListener(this);
+ final Intent intent = getIntent();
+ this.retrySmsAfter = intent != null ? intent.getLongExtra(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter) : this.retrySmsAfter;
+ if (this.retrySmsAfter > 0) {
+ mHandler.post(SMS_TIMEOUT_UPDATER);
+ }
+ if (this.retryVerificationAfter > 0) {
+ mHandler.post(VERIFICATION_TIMEOUT_UPDATER);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER);
+ mHandler.removeCallbacks(VERIFICATION_TIMEOUT_UPDATER);
+ clipboardManager.removePrimaryClipChangedListener(this);
+ if (xmppConnectionService != null) {
+ xmppConnectionService.getQuickConversationsService().removeOnVerificationListener(this);
+ xmppConnectionService.getQuickConversationsService().removeOnVerificationRequestedListener(this);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (pinEntryWrapper.isEmpty()) {
+ pastePinFromClipboard();
+ }
+ }
+
+ private void pastePinFromClipboard() {
+ final ClipDescription description = clipboardManager != null ? clipboardManager.getPrimaryClipDescription() : null;
+ if (description != null && description.hasMimeType(MIMETYPE_TEXT_PLAIN)) {
+ final ClipData primaryClip = clipboardManager.getPrimaryClip();
+ if (primaryClip != null && primaryClip.getItemCount() > 0) {
+ final CharSequence clip = primaryClip.getItemAt(0).getText();
+ if (PinEntryWrapper.isValidPin(clip) && !clip.toString().equals(this.pasted)) {
+ this.pasted = clip.toString();
+ pinEntryWrapper.setPin(clip.toString());
+ final Snackbar snackbar = Snackbar.make(binding.coordinator, R.string.possible_pin, Snackbar.LENGTH_LONG);
+ snackbar.setAction(R.string.undo, v -> pinEntryWrapper.clear());
+ snackbar.show();
+ }
+ }
+ }
+ }
+
+ private void performPostVerificationRedirect() {
+ Intent intent = new Intent(this, PublishProfilePictureActivity.class);
+ intent.putExtra(PublishProfilePictureActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+ intent.putExtra("setup", true);
+ startActivity(intent);
+ finish();
+ }
+
+ @Override
+ public void onPrimaryClipChanged() {
+ this.pasted = null;
+ if (pinEntryWrapper.isEmpty()) {
+ pastePinFromClipboard();
+ }
+ }
+
+ @Override
+ public void onVerificationFailed(final int code) {
+ runOnUiThread(() -> {
+ setVerifyingState(false);
+ if (code == 401) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.incorrect_pin);
+ builder.setPositiveButton(R.string.ok, null);
+ builder.create().show();
+ } else {
+ ApiDialogHelper.createError(this, code).show();
+ }
+ });
+ }
+
+ @Override
+ public void onVerificationSucceeded() {
+ runOnUiThread(this::performPostVerificationRedirect);
+ }
+
+ @Override
+ public void onVerificationRetryAt(long timestamp) {
+ this.retryVerificationAfter = timestamp;
+ runOnUiThread(() -> {
+ ApiDialogHelper.createTooManyAttempts(this).show();
+ setVerifyingState(false);
+ });
+ mHandler.removeCallbacks(VERIFICATION_TIMEOUT_UPDATER);
+ runOnUiThread(VERIFICATION_TIMEOUT_UPDATER);
+ }
+
+ //send sms again button callback
+ @Override
+ public void onVerificationRequestFailed(int code) {
+ runOnUiThread(() -> {
+ setRequestingVerificationState(false);
+ ApiDialogHelper.createError(this, code).show();
+ });
+ }
+
+ //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_another_sms);
+ builder.setPositiveButton(R.string.ok, null);
+ builder.create().show();
+ });
+ }
+
+ @Override
+ public void onVerificationRequestedRetryAt(long timestamp) {
+ this.retrySmsAfter = timestamp;
+ runOnUiThread(() -> {
+ ApiDialogHelper.createRateLimited(this, timestamp).show();
+ setRequestingVerificationState(false);
+ });
+ mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER);
+ runOnUiThread(SMS_TIMEOUT_UPDATER);
+ }
+}
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/adapter/CountryAdapter.java b/src/quicksy/java/eu/siacs/conversations/ui/adapter/CountryAdapter.java
new file mode 100644
index 000000000..266f9e631
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/ui/adapter/CountryAdapter.java
@@ -0,0 +1,70 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.databinding.DataBindingUtil;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.CountryItemBinding;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+
+public class CountryAdapter extends RecyclerView.Adapter<CountryAdapter.CountryViewHolder> {
+
+ private final List<PhoneNumberUtilWrapper.Country> countries;
+
+ private OnCountryClicked onCountryClicked;
+
+ public CountryAdapter(List<PhoneNumberUtilWrapper.Country> countries) {
+ this.countries = countries;
+ }
+
+ @NonNull
+ @Override
+ public CountryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+ CountryItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.country_item, parent, false);
+ return new CountryViewHolder(binding);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CountryViewHolder holder, int position) {
+ final PhoneNumberUtilWrapper.Country county = countries.get(position);
+ holder.binding.country.setText(county.getName());
+ holder.binding.countryCode.setText(county.getCode());
+ holder.itemView.setOnClickListener(v -> {
+ if (onCountryClicked != null) {
+ onCountryClicked.onCountryClicked(county);
+ }
+ });
+ }
+
+ public void setOnCountryClicked(OnCountryClicked listener) {
+ this.onCountryClicked = listener;
+ }
+
+
+ @Override
+ public int getItemCount() {
+ return countries.size();
+ }
+
+
+ class CountryViewHolder extends RecyclerView.ViewHolder {
+
+ private final CountryItemBinding binding;
+
+ CountryViewHolder(CountryItemBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+ }
+
+ public interface OnCountryClicked {
+ void onCountryClicked(PhoneNumberUtilWrapper.Country country);
+ }
+
+}
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java b/src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java
new file mode 100644
index 000000000..3b49962a6
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java
@@ -0,0 +1,240 @@
+package eu.siacs.conversations.ui.drawable; /**
+ * Copyright 2016 Ali Muzaffar
+ * <p/>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+
+public class TextDrawable extends Drawable implements TextWatcher {
+ private WeakReference<TextView> ref;
+ private String mText;
+ private Paint mPaint;
+ private Rect mHeightBounds;
+ private boolean mBindToViewPaint = false;
+ private float mPrevTextSize = 0;
+ private boolean mInitFitText = false;
+ private boolean mFitTextEnabled = false;
+
+ /**
+ * Create a TextDrawable using the given paint object and string
+ *
+ * @param paint
+ * @param s
+ */
+ public TextDrawable(Paint paint, String s) {
+ mText = s;
+ mPaint = new Paint(paint);
+ mHeightBounds = new Rect();
+ init();
+ }
+
+ /**
+ * Create a TextDrawable. This uses the given TextView to initialize paint and has initial text
+ * that will be drawn. Initial text can also be useful for reserving space that may otherwise
+ * not be available when setting compound drawables.
+ *
+ * @param tv The TextView / EditText using to initialize this drawable
+ * @param initialText Optional initial text to display
+ * @param bindToViewsText Should this drawable mirror the text in the TextView
+ * @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
+ * Note, this will override any changes made using setColorFilter or setAlpha.
+ */
+ public TextDrawable(TextView tv, String initialText, boolean bindToViewsText, boolean bindToViewsPaint) {
+ this(tv.getPaint(), initialText);
+ ref = new WeakReference<>(tv);
+ if (bindToViewsText || bindToViewsPaint) {
+ if (bindToViewsText) {
+ tv.addTextChangedListener(this);
+ }
+ mBindToViewPaint = bindToViewsPaint;
+ }
+ }
+
+ /**
+ * Create a TextDrawable. This uses the given TextView to initialize paint and the text that
+ * will be drawn.
+ *
+ * @param tv The TextView / EditText using to initialize this drawable
+ * @param bindToViewsText Should this drawable mirror the text in the TextView
+ * @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
+ * Note, this will override any changes made using setColorFilter or setAlpha.
+ */
+ public TextDrawable(TextView tv, boolean bindToViewsText, boolean bindToViewsPaint) {
+ this(tv, tv.getText().toString(), false, false);
+ }
+
+ /**
+ * Use the provided TextView/EditText to initialize the drawable.
+ * The Drawable will copy the Text and the Paint properties, however it will from that
+ * point on be independant of the TextView.
+ *
+ * @param tv a TextView or EditText or any of their children.
+ */
+ public TextDrawable(TextView tv) {
+ this(tv, false, false);
+ }
+
+ /**
+ * Use the provided TextView/EditText to initialize the drawable.
+ * The Drawable will copy the Paint properties, and use the provided text to initialise itself.
+ *
+ * @param tv a TextView or EditText or any of their children.
+ * @param s The String to draw
+ */
+ public TextDrawable(TextView tv, String s) {
+ this(tv, s, false, false);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mBindToViewPaint && ref.get() != null) {
+ Paint p = ref.get().getPaint();
+ canvas.drawText(mText, 0, getBounds().height(), p);
+ } else {
+ if (mInitFitText) {
+ fitTextAndInit();
+ }
+ canvas.drawText(mText, 0, getBounds().height(), mPaint);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public int getOpacity() {
+ int alpha = mPaint.getAlpha();
+ if (alpha == 0) {
+ return PixelFormat.TRANSPARENT;
+ } else if (alpha == 255) {
+ return PixelFormat.OPAQUE;
+ } else {
+ return PixelFormat.TRANSLUCENT;
+ }
+ }
+
+ private void init() {
+ Rect bounds = getBounds();
+ //We want to use some character to determine the max height of the text.
+ //Otherwise if we draw something like "..." they will appear centered
+ //Here I'm just going to use the entire alphabet to determine max height.
+ mPaint.getTextBounds("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+", 0, 1, mHeightBounds);
+ //This doesn't account for leading or training white spaces.
+ //mPaint.getTextBounds(mText, 0, mText.length(), bounds);
+ float width = mPaint.measureText(mText);
+ bounds.top = mHeightBounds.top;
+ bounds.bottom = mHeightBounds.bottom;
+ bounds.right = (int) width;
+ bounds.left = 0;
+ setBounds(bounds);
+ }
+
+ public void setPaint(Paint paint) {
+ mPaint = new Paint(paint);
+ //Since this can change the font used, we need to recalculate bounds.
+ if (mFitTextEnabled && !mInitFitText) {
+ fitTextAndInit();
+ } else {
+ init();
+ }
+ invalidateSelf();
+ }
+
+ public Paint getPaint() {
+ return mPaint;
+ }
+
+ public void setText(String text) {
+ mText = text;
+ //Since this can change the bounds of the text, we need to recalculate.
+ if (mFitTextEnabled && !mInitFitText) {
+ fitTextAndInit();
+ } else {
+ init();
+ }
+ invalidateSelf();
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ setText(s.toString());
+ }
+
+ /**
+ * Make the TextDrawable match the width of the View it's associated with.
+ * <p/>
+ * Note: While this option will not work if bindToViewPaint is true.
+ *
+ * @param fitText
+ */
+ public void setFillText(boolean fitText) {
+ mFitTextEnabled = fitText;
+ if (fitText) {
+ mPrevTextSize = mPaint.getTextSize();
+ if (ref.get() != null) {
+ if (ref.get().getWidth() > 0) {
+ fitTextAndInit();
+ } else {
+ mInitFitText = true;
+ }
+ }
+ } else {
+ if (mPrevTextSize > 0) {
+ mPaint.setTextSize(mPrevTextSize);
+ }
+ init();
+ }
+ }
+
+ private void fitTextAndInit() {
+ float fitWidth = ref.get().getWidth();
+ float textWidth = mPaint.measureText(mText);
+ float multi = fitWidth / textWidth;
+ mPaint.setTextSize(mPaint.getTextSize() * multi);
+ mInitFitText = false;
+ init();
+ }
+
+} \ No newline at end of file
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java b/src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java
new file mode 100644
index 000000000..74dc49c20
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java
@@ -0,0 +1,83 @@
+package eu.siacs.conversations.ui.util;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.support.annotation.StringRes;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.utils.TimeframeUtils;
+
+public class ApiDialogHelper {
+
+ public static Dialog createError(final Context context, final int code) {
+ @StringRes final int res;
+ switch (code) {
+ case QuickConversationsService.API_ERROR_AIRPLANE_MODE:
+ res = R.string.no_network_connection;
+ break;
+ case QuickConversationsService.API_ERROR_OTHER:
+ res = R.string.unknown_api_error_network;
+ break;
+ case QuickConversationsService.API_ERROR_CONNECT:
+ res = R.string.unable_to_connect_to_server;
+ break;
+ case QuickConversationsService.API_ERROR_SSL_HANDSHAKE:
+ res = R.string.unable_to_establish_secure_connection;
+ break;
+ case QuickConversationsService.API_ERROR_UNKNOWN_HOST:
+ res = R.string.unable_to_find_server;
+ break;
+ case 400:
+ res = R.string.invalid_user_input;
+ break;
+ case 403:
+ res = R.string.the_app_is_out_of_date;
+ break;
+ case 502:
+ case 503:
+ case 504:
+ res = R.string.temporarily_unavailable;
+ break;
+ default:
+ res = R.string.unknown_api_error_response;
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(res);
+ if (code == 403 && resolvable(context, getMarketViewIntent(context))) {
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.update, (dialog, which) -> context.startActivity(getMarketViewIntent(context)));
+ } else {
+ builder.setPositiveButton(R.string.ok, null);
+ }
+ return builder.create();
+ }
+
+ public static Dialog createRateLimited(final Context context, final long timestamp) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.rate_limited);
+ builder.setMessage(context.getString(R.string.try_again_in_x, TimeframeUtils.resolve(context, timestamp - SystemClock.elapsedRealtime())));
+ builder.setPositiveButton(R.string.ok, null);
+ return builder.create();
+ }
+
+ public static Dialog createTooManyAttempts(final Context context) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.too_many_attempts);
+ builder.setPositiveButton(R.string.ok, null);
+ return builder.create();
+ }
+
+ private static Intent getMarketViewIntent(Context context) {
+ return new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName()));
+ }
+
+ private static boolean resolvable(Context context, Intent intent) {
+ return context.getPackageManager().queryIntentActivities(intent, 0).size() > 0;
+ }
+}
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java b/src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java
new file mode 100644
index 000000000..17da72798
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java
@@ -0,0 +1,149 @@
+package eu.siacs.conversations.ui.util;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+
+public class PinEntryWrapper {
+
+ private static Pattern PIN_STRING_PATTERN = Pattern.compile("^[0-9]{6}$");
+
+ private final List<EditText> digits = new ArrayList<>();
+
+ private final TextWatcher textWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ int current = -1;
+ for (int i = 0; i < digits.size(); ++i) {
+ if (s.hashCode() == digits.get(i).getText().hashCode()) {
+ current = i;
+ }
+ }
+ if (current == -1) {
+ return;
+ }
+ if (s.length() > 0) {
+ if (current < digits.size() - 1) {
+ digits.get(current + 1).requestFocus();
+ }
+ } else {
+ int focusOn = current;
+ for (int i = current - 1; i >= 0; --i) {
+ if (digits.get(i).getText().length() == 0) {
+ focusOn = i;
+ } else {
+ break;
+ }
+ }
+ digits.get(focusOn).requestFocus();
+ }
+ }
+ };
+
+ private final View.OnKeyListener keyListener = (v, keyCode, event) -> {
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return false;
+ }
+ if (v instanceof EditText) {
+ final EditText editText = (EditText) v;
+ final boolean cursorAtZero = editText.getSelectionEnd() == 0 && editText.getSelectionStart() == 0;
+ if (keyCode == KeyEvent.KEYCODE_DEL && (cursorAtZero || editText.getText().length() == 0)) {
+ final int current = digits.indexOf(editText);
+ for (int i = current - 1; i >= 0; --i) {
+ if (digits.get(i).getText().length() > 0) {
+ digits.get(i).getText().clear();
+ return true;
+ }
+ }
+ if (current != 0) {
+ digits.get(0).requestFocus();
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ public PinEntryWrapper(LinearLayout linearLayout) {
+ for (int i = 0; i < linearLayout.getChildCount(); ++i) {
+ View view = linearLayout.getChildAt(i);
+ if (view instanceof EditText) {
+ this.digits.add((EditText) view);
+ }
+ }
+ registerListeners();
+ }
+
+ private void registerListeners() {
+ for (EditText editText : digits) {
+ editText.addTextChangedListener(textWatcher);
+ editText.setOnKeyListener(keyListener);
+ }
+ }
+
+ public String getPin() {
+ char[] chars = new char[digits.size()];
+ for (int i = 0; i < chars.length; ++i) {
+ final String input = digits.get(i).getText().toString();
+ chars[i] = input.length() != 1 ? ' ' : input.charAt(0);
+ }
+ return String.valueOf(chars);
+ }
+
+ public void setPin(String pin) {
+ char[] chars = pin.toCharArray();
+ for (int i = 0; i < digits.size(); ++i) {
+ if (i < chars.length) {
+ final Editable editable = digits.get(i).getText();
+ editable.clear();
+ editable.append(Character.isDigit(chars[i]) ? String.valueOf(chars[i]) : "");
+ }
+ }
+ }
+
+ public void setEnabled(boolean enabled) {
+ for(EditText digit : digits) {
+ digit.setEnabled(enabled);
+ digit.setCursorVisible(enabled);
+ digit.setFocusable(enabled);
+ digit.setFocusableInTouchMode(enabled);
+ }
+ }
+
+ public boolean isEmpty() {
+ for (EditText digit : digits) {
+ if (digit.getText().length() > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean isValidPin(CharSequence pin) {
+ return pin != null && PIN_STRING_PATTERN.matcher(pin).matches();
+ }
+
+ public void clear() {
+ for (int i = digits.size() - 1; i >= 0; --i) {
+ digits.get(i).getText().clear();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/quicksy/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java b/src/quicksy/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java
new file mode 100644
index 000000000..bccad767f
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java
@@ -0,0 +1,116 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Context;
+import android.telephony.TelephonyManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import io.michaelrocks.libphonenumber.android.NumberParseException;
+import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
+import io.michaelrocks.libphonenumber.android.Phonenumber;
+import rocks.xmpp.addr.Jid;
+
+public class PhoneNumberUtilWrapper {
+
+ private static volatile PhoneNumberUtil instance;
+
+
+ public static String getCountryForCode(String code) {
+ Locale locale = new Locale("", code);
+ return locale.getDisplayCountry();
+ }
+
+ public static String getUserCountry(Context context) {
+ try {
+ final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ final String simCountry = tm.getSimCountryIso();
+ if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
+ return simCountry.toUpperCase(Locale.US);
+ } else if (tm.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { // device is not 3G (would be unreliable)
+ String networkCountry = tm.getNetworkCountryIso();
+ if (networkCountry != null && networkCountry.length() == 2) { // network country code is available
+ return networkCountry.toUpperCase(Locale.US);
+ }
+ }
+ } catch (Exception e) {
+ // fallthrough
+ }
+ Locale locale = Locale.getDefault();
+ return locale.getCountry();
+ }
+
+ public static String toFormattedPhoneNumber(Context context, Jid jid) {
+ try {
+ 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)));
+ }
+
+ public static String normalize(Context context, Phonenumber.PhoneNumber phoneNumber) {
+ return getInstance(context).format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+ }
+
+ public static PhoneNumberUtil getInstance(final Context context) {
+ PhoneNumberUtil localInstance = instance;
+ if (localInstance == null) {
+ synchronized (PhoneNumberUtilWrapper.class) {
+ localInstance = instance;
+ if (localInstance == null) {
+ instance = localInstance = PhoneNumberUtil.createInstance(context);
+ }
+
+ }
+ }
+ return localInstance;
+ }
+
+ public static List<Country> getCountries(final Context context) {
+ List<Country> countries = new ArrayList<>();
+ for (String region : getInstance(context).getSupportedRegions()) {
+ countries.add(new Country(region, getInstance(context).getCountryCodeForRegion(region)));
+ }
+ return countries;
+
+ }
+
+ public static class Country implements Comparable<Country> {
+ private final String name;
+ private final String region;
+ private final int code;
+
+ Country(String region, int code) {
+ this.name = getCountryForCode(region);
+ this.region = region;
+ this.code = code;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public String getCode() {
+ return '+' + String.valueOf(code);
+ }
+
+ @Override
+ public int compareTo(Country o) {
+ return name.compareTo(o.name);
+ }
+ }
+
+}
diff --git a/src/quicksy/java/eu/siacs/conversations/utils/SignupUtils.java b/src/quicksy/java/eu/siacs/conversations/utils/SignupUtils.java
new file mode 100644
index 000000000..ac796ea99
--- /dev/null
+++ b/src/quicksy/java/eu/siacs/conversations/utils/SignupUtils.java
@@ -0,0 +1,37 @@
+package eu.siacs.conversations.utils;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.ConversationsActivity;
+import eu.siacs.conversations.ui.EnterPhoneNumberActivity;
+import eu.siacs.conversations.ui.StartConversationActivity;
+import eu.siacs.conversations.ui.VerifyActivity;
+
+public class SignupUtils {
+
+ public static Intent getSignUpIntent(Activity activity) {
+ final Intent intent = new Intent(activity, EnterPhoneNumberActivity.class);
+ return intent;
+ }
+
+ public static Intent getRedirectionIntent(ConversationsActivity activity) {
+ final Intent intent;
+ final Account account = AccountUtils.getFirst(activity.xmppConnectionService);
+ if (account != null) {
+ if (account.isOptionSet(Account.OPTION_UNVERIFIED)) {
+ intent = new Intent(activity, VerifyActivity.class);
+ } else {
+ intent = new Intent(activity, StartConversationActivity.class);
+ }
+ } else {
+ intent = getSignUpIntent(activity);
+
+ }
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ return intent;
+ }
+} \ No newline at end of file