EnterPhoneNumberActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
  4
  5import android.Manifest;
  6import android.app.AlertDialog;
  7import android.content.Intent;
  8import android.os.Bundle;
  9import android.text.Editable;
 10import android.text.Html;
 11import android.text.TextUtils;
 12import android.text.TextWatcher;
 13import android.util.Log;
 14import android.view.KeyEvent;
 15import android.view.Menu;
 16import android.view.MenuItem;
 17import android.view.View;
 18import android.widget.EditText;
 19import android.widget.Toast;
 20import androidx.annotation.NonNull;
 21import androidx.databinding.DataBindingUtil;
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.R;
 24import eu.siacs.conversations.databinding.ActivityEnterNumberBinding;
 25import eu.siacs.conversations.entities.Account;
 26import eu.siacs.conversations.services.QuickConversationsService;
 27import eu.siacs.conversations.ui.drawable.TextDrawable;
 28import eu.siacs.conversations.ui.util.ApiDialogHelper;
 29import eu.siacs.conversations.utils.AccountUtils;
 30import eu.siacs.conversations.utils.LocationProvider;
 31import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 32import io.michaelrocks.libphonenumber.android.NumberParseException;
 33import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
 34import io.michaelrocks.libphonenumber.android.Phonenumber;
 35import java.util.Arrays;
 36import java.util.concurrent.atomic.AtomicBoolean;
 37
 38public class EnterPhoneNumberActivity extends XmppActivity
 39        implements QuickConversationsService.OnVerificationRequested {
 40
 41    private static final int REQUEST_CHOOSE_COUNTRY = 0x1234;
 42    private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
 43
 44    private ActivityEnterNumberBinding binding;
 45
 46    private final AtomicBoolean redirectInProgress = new AtomicBoolean(false);
 47
 48    private String region = null;
 49    private final TextWatcher countryCodeTextWatcher =
 50            new TextWatcher() {
 51                @Override
 52                public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
 53
 54                @Override
 55                public void onTextChanged(CharSequence s, int start, int before, int count) {}
 56
 57                @Override
 58                public void afterTextChanged(Editable editable) {
 59                    final String text = editable.toString();
 60                    try {
 61                        final int oldCode =
 62                                region != null
 63                                        ? PhoneNumberUtilWrapper.getInstance(
 64                                                        EnterPhoneNumberActivity.this)
 65                                                .getCountryCodeForRegion(region)
 66                                        : 0;
 67                        final int code = Integer.parseInt(text);
 68                        if (oldCode != code) {
 69                            region =
 70                                    PhoneNumberUtilWrapper.getInstance(
 71                                                    EnterPhoneNumberActivity.this)
 72                                            .getRegionCodeForCountryCode(code);
 73                        }
 74                        if ("ZZ".equals(region)) {
 75                            binding.country.setText(
 76                                    TextUtils.isEmpty(text)
 77                                            ? R.string.choose_a_country
 78                                            : R.string.invalid_country_code);
 79                        } else {
 80                            binding.number.requestFocus();
 81                            binding.country.setText(
 82                                    PhoneNumberUtilWrapper.getCountryForCode(region));
 83                        }
 84                    } catch (NumberFormatException e) {
 85                        binding.country.setText(
 86                                TextUtils.isEmpty(text)
 87                                        ? R.string.choose_a_country
 88                                        : R.string.invalid_country_code);
 89                    }
 90                }
 91            };
 92    private boolean requestingVerification = false;
 93
 94    @Override
 95    protected void refreshUiReal() {}
 96
 97    @Override
 98    public void onBackendConnected() {
 99        xmppConnectionService
100                .getQuickConversationsService()
101                .addOnVerificationRequestedListener(this);
102        final Account account = AccountUtils.getFirst(xmppConnectionService);
103        if (account != null) {
104            runOnUiThread(this::performRedirectToVerificationActivity);
105        }
106    }
107
108    @Override
109    protected void onCreate(final Bundle savedInstanceState) {
110        super.onCreate(savedInstanceState);
111
112        String region = savedInstanceState != null ? savedInstanceState.getString("region") : null;
113        boolean requestingVerification =
114                savedInstanceState != null
115                        && savedInstanceState.getBoolean("requesting_verification", false);
116        if (region != null) {
117            this.region = region;
118        } else {
119            this.region = LocationProvider.getUserCountry(this);
120        }
121
122        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_number);
123        this.binding.countryCode.setCompoundDrawables(
124                new TextDrawable(this.binding.countryCode, "+"), null, null, null);
125        this.binding.country.setOnClickListener(this::onSelectCountryClick);
126        this.binding.next.setOnClickListener(this::onNextClick);
127        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
128        setSupportActionBar(this.binding.toolbar);
129        this.binding.countryCode.addTextChangedListener(this.countryCodeTextWatcher);
130        this.binding.countryCode.setText(
131                String.valueOf(
132                        PhoneNumberUtilWrapper.getInstance(this)
133                                .getCountryCodeForRegion(this.region)));
134        this.binding.number.setOnKeyListener(
135                (v, keyCode, event) -> {
136                    if (event.getAction() != KeyEvent.ACTION_DOWN) {
137                        return false;
138                    }
139                    final EditText editText = (EditText) v;
140                    final boolean cursorAtZero =
141                            editText.getSelectionEnd() == 0 && editText.getSelectionStart() == 0;
142                    if (keyCode == KeyEvent.KEYCODE_DEL
143                            && (cursorAtZero || editText.getText().length() == 0)) {
144                        final Editable countryCode = this.binding.countryCode.getText();
145                        if (countryCode.length() > 0) {
146                            countryCode.delete(countryCode.length() - 1, countryCode.length());
147                            this.binding.countryCode.setSelection(countryCode.length());
148                        }
149                        this.binding.countryCode.requestFocus();
150                        return true;
151                    }
152                    return false;
153                });
154        setRequestingVerificationState(requestingVerification);
155    }
156
157    @Override
158    public boolean onCreateOptionsMenu(final Menu menu) {
159        getMenuInflater().inflate(R.menu.verify_phone_number_menu, menu);
160        return super.onCreateOptionsMenu(menu);
161    }
162
163    @Override
164    public boolean onOptionsItemSelected(final MenuItem item) {
165        if (item.getItemId() == R.id.action_import_backup) {
166            if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
167                startActivity(new Intent(this, ImportBackupActivity.class));
168            }
169            return true;
170        } else {
171            return super.onOptionsItemSelected(item);
172        }
173    }
174
175    @Override
176    public void onRequestPermissionsResult(
177            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
178        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
179        UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
180        if (grantResults.length > 0) {
181            if (allGranted(grantResults)) {
182                if (requestCode == REQUEST_IMPORT_BACKUP) {
183                    startActivity(new Intent(this, ImportBackupActivity.class));
184                }
185            } else if (Arrays.asList(permissions)
186                    .contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
187                Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
188            }
189        }
190    }
191
192    @Override
193    public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
194        if (this.region != null) {
195            savedInstanceState.putString("region", this.region);
196        }
197        savedInstanceState.putBoolean("requesting_verification", this.requestingVerification);
198        super.onSaveInstanceState(savedInstanceState);
199    }
200
201    @Override
202    public void onStop() {
203        if (xmppConnectionService != null) {
204            xmppConnectionService
205                    .getQuickConversationsService()
206                    .removeOnVerificationRequestedListener(this);
207        }
208        super.onStop();
209    }
210
211    private void onNextClick(View v) {
212        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
213        try {
214            final Editable number = this.binding.number.getText();
215            final String input = number.toString();
216            final Phonenumber.PhoneNumber phoneNumber =
217                    PhoneNumberUtilWrapper.getInstance(this).parse(input, region);
218            this.binding.countryCode.setText(String.valueOf(phoneNumber.getCountryCode()));
219            number.clear();
220            number.append(String.valueOf(phoneNumber.getNationalNumber()));
221            final String formattedPhoneNumber =
222                    PhoneNumberUtilWrapper.getInstance(this)
223                            .format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
224                            .replace(' ', '\u202F');
225
226            if (PhoneNumberUtilWrapper.getInstance(this).isValidNumber(phoneNumber)) {
227                builder.setMessage(
228                        Html.fromHtml(
229                                getString(R.string.we_will_be_verifying, formattedPhoneNumber)));
230                builder.setNegativeButton(R.string.edit, null);
231                builder.setPositiveButton(
232                        R.string.ok, (dialog, which) -> onPhoneNumberEntered(phoneNumber));
233            } else {
234                builder.setMessage(
235                        getString(R.string.not_a_valid_phone_number, formattedPhoneNumber));
236                builder.setPositiveButton(R.string.ok, null);
237            }
238            Log.d(Config.LOGTAG, phoneNumber.toString());
239        } catch (NumberParseException e) {
240            builder.setMessage(R.string.please_enter_your_phone_number);
241            builder.setPositiveButton(R.string.ok, null);
242        }
243        builder.create().show();
244    }
245
246    private void onSelectCountryClick(View view) {
247        final Intent intent = new Intent(this, ChooseCountryActivity.class);
248        startActivityForResult(intent, REQUEST_CHOOSE_COUNTRY);
249    }
250
251    private void onPhoneNumberEntered(Phonenumber.PhoneNumber phoneNumber) {
252        setRequestingVerificationState(true);
253        xmppConnectionService.getQuickConversationsService().requestVerification(phoneNumber);
254    }
255
256    private void setRequestingVerificationState(boolean requesting) {
257        this.requestingVerification = requesting;
258        this.binding.countryCode.setEnabled(!requesting);
259        this.binding.country.setEnabled(!requesting);
260        this.binding.number.setEnabled(!requesting);
261        this.binding.next.setEnabled(!requesting);
262        this.binding.next.setText(requesting ? R.string.requesting_sms : R.string.next);
263        this.binding.progressBar.setVisibility(requesting ? View.VISIBLE : View.GONE);
264        this.binding.progressBar.setIndeterminate(requesting);
265    }
266
267    @Override
268    public void onActivityResult(int requestCode, int resultCode, final Intent data) {
269        super.onActivityResult(requestCode, resultCode, data);
270        if (resultCode == RESULT_OK && requestCode == REQUEST_CHOOSE_COUNTRY) {
271            final String region = data.getStringExtra("region");
272            if (region != null) {
273                this.region = region;
274                final int countryCode =
275                        PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(region);
276                this.binding.countryCode.setText(String.valueOf(countryCode));
277            }
278        }
279    }
280
281    private void performRedirectToVerificationActivity(long timestamp) {
282        if (redirectInProgress.compareAndSet(false, true)) {
283            Intent intent = new Intent(this, VerifyActivity.class);
284            intent.putExtra(VerifyActivity.EXTRA_RETRY_SMS_AFTER, timestamp);
285            startActivity(intent);
286            finish();
287        }
288    }
289
290    private void performRedirectToVerificationActivity() {
291        if (redirectInProgress.compareAndSet(false, true)) {
292            startActivity(new Intent(this, VerifyActivity.class));
293            finish();
294        }
295    }
296
297    @Override
298    public void onVerificationRequestFailed(int code) {
299        runOnUiThread(
300                () -> {
301                    setRequestingVerificationState(false);
302                    ApiDialogHelper.createError(this, code).show();
303                });
304    }
305
306    @Override
307    public void onVerificationRequested() {
308        runOnUiThread(this::performRedirectToVerificationActivity);
309    }
310
311    @Override
312    public void onVerificationRequestedRetryAt(long timestamp) {
313        runOnUiThread(() -> performRedirectToVerificationActivity(timestamp));
314    }
315}