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}