diff options
author | Daniel Gultsch <daniel@gultsch.de> | 2018-10-28 14:34:17 +0300 |
---|---|---|
committer | Daniel Gultsch <daniel@gultsch.de> | 2018-10-31 15:33:55 +0300 |
commit | 87cc53b8b5fcad666a981676f522ba07d28c805c (patch) | |
tree | 7a035dd3c7c13694b90d422afe5ac358fb23402d /src/quicksy/java | |
parent | a49a5790c79c1a9d59e1934159d0d67c326d1322 (diff) |
renamed build flavors
Diffstat (limited to 'src/quicksy/java')
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 |