Java tutorial
/* * Kontalk Android client * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org> * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.ui; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.net.SocketException; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.zip.ZipInputStream; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.folderselector.FileChooserDialog; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.NumberParseException.ErrorType; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import org.jivesoftware.smack.util.StringUtils; import org.jxmpp.util.XmppStringUtils; import org.spongycastle.openpgp.PGPException; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.Handler; import android.provider.ContactsContract; import android.support.annotation.NonNull; import android.support.v4.content.LocalBroadcastManager; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Base64; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.Button; import android.widget.EditText; import android.widget.Spinner; import android.widget.Toast; import org.kontalk.BuildConfig; import org.kontalk.Kontalk; import org.kontalk.Log; import org.kontalk.R; import org.kontalk.authenticator.Authenticator; import org.kontalk.client.EndpointServer; import org.kontalk.client.NumberValidator; import org.kontalk.client.NumberValidator.NumberValidatorListener; import org.kontalk.crypto.PGPUidMismatchException; import org.kontalk.crypto.PGPUserID; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.PersonalKeyImporter; import org.kontalk.crypto.PersonalKeyPack; import org.kontalk.crypto.X509Bridge; import org.kontalk.provider.Keyring; import org.kontalk.reporting.ReportingManager; import org.kontalk.service.KeyPairGeneratorService; import org.kontalk.service.KeyPairGeneratorService.KeyGeneratorReceiver; import org.kontalk.service.KeyPairGeneratorService.PersonalKeyRunnable; import org.kontalk.sync.SyncAdapter; import org.kontalk.ui.adapter.CountryCodesAdapter; import org.kontalk.ui.adapter.CountryCodesAdapter.CountryCode; import org.kontalk.ui.prefs.PreferencesActivity; import org.kontalk.util.MessageUtils; import org.kontalk.util.Preferences; import org.kontalk.util.SystemUtils; /** Number validation activity. */ public class NumberValidation extends AccountAuthenticatorActionBarActivity implements NumberValidatorListener, FileChooserDialog.FileCallback { static final String TAG = NumberValidation.class.getSimpleName(); public static final int REQUEST_MANUAL_VALIDATION = 771; public static final int REQUEST_VALIDATION_CODE = 772; public static final int RESULT_FALLBACK = RESULT_FIRST_USER + 1; public static final String PARAM_FROM_INTERNAL = "org.kontalk.internal"; public static final String PARAM_PUBLICKEY = "org.kontalk.publickey"; public static final String PARAM_PRIVATEKEY = "org.kontalk.privatekey"; public static final String PARAM_SERVER_URI = "org.kontalk.server"; public static final String PARAM_CHALLENGE = "org.kontalk.challenge"; public static final String PARAM_TRUSTED_KEYS = "org.kontalk.trustedkeys"; private AccountManager mAccountManager; private EditText mNameText; private Spinner mCountryCode; private EditText mPhone; private Button mValidateButton; private MaterialDialog mProgress; private CharSequence mProgressMessage; NumberValidator mValidator; Handler mHandler; private String mPhoneNumber; private String mName; PersonalKey mKey; private String mPassphrase; private byte[] mImportedPublicKey; private byte[] mImportedPrivateKey; Map<String, Keyring.TrustedFingerprint> mTrustedKeys; private boolean mForce; private LocalBroadcastManager lbm; /** Will be true when resuming for a fallback registration. */ private boolean mClearState; boolean mFromInternal; /** Runnable for delaying initial manual sync starter. */ Runnable mSyncStart; private boolean mSyncing; private KeyGeneratorReceiver mKeyReceiver; private static final class RetainData { NumberValidator validator; /** @deprecated Use saved instance state. */ @Deprecated CharSequence progressMessage; /** @deprecated Use saved instance state. */ @Deprecated boolean syncing; RetainData() { } } /** * Compatibility method for {@link PhoneNumberUtil#getSupportedRegions()}. * This was introduced because crappy Honeycomb has an old version of * libphonenumber, therefore Dalvik will insist on we using it. * In case getSupportedRegions doesn't exist, getSupportedCountries will be * used. */ @SuppressWarnings("unchecked") private Set<String> getSupportedRegions(PhoneNumberUtil util) { try { return (Set<String>) util.getClass().getMethod("getSupportedRegions").invoke(util); } catch (NoSuchMethodException e) { try { return (Set<String>) util.getClass().getMethod("getSupportedCountries").invoke(util); } catch (Exception helpme) { // ignored } } catch (Exception e) { // ignored } return new HashSet<>(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.number_validation); setupToolbar(false, false); mAccountManager = AccountManager.get(this); mHandler = new Handler(); lbm = LocalBroadcastManager.getInstance(getApplicationContext()); final Intent intent = getIntent(); mFromInternal = intent.getBooleanExtra(PARAM_FROM_INTERNAL, false); mNameText = (EditText) findViewById(R.id.name); mCountryCode = (Spinner) findViewById(R.id.phone_cc); mPhone = (EditText) findViewById(R.id.phone_number); mValidateButton = (Button) findViewById(R.id.button_validate); // populate country codes final CountryCodesAdapter ccList = new CountryCodesAdapter(this, android.R.layout.simple_list_item_1, android.R.layout.simple_spinner_dropdown_item); PhoneNumberUtil util = PhoneNumberUtil.getInstance(); Set<String> ccSet = getSupportedRegions(util); for (String cc : ccSet) ccList.add(cc); ccList.sort(new Comparator<CountryCodesAdapter.CountryCode>() { public int compare(CountryCodesAdapter.CountryCode lhs, CountryCodesAdapter.CountryCode rhs) { return lhs.regionName.compareTo(rhs.regionName); } }); mCountryCode.setAdapter(ccList); mCountryCode.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { ccList.setSelected(position); } public void onNothingSelected(AdapterView<?> parent) { // TODO Auto-generated method stub } }); // FIXME this doesn't consider creation because of configuration change PhoneNumber myNum = NumberValidator.getMyNumber(this); if (myNum != null) { CountryCode cc = new CountryCode(); cc.regionCode = util.getRegionCodeForNumber(myNum); if (cc.regionCode == null) cc.regionCode = util.getRegionCodeForCountryCode(myNum.getCountryCode()); mCountryCode.setSelection(ccList.getPositionForId(cc)); mPhone.setText(String.valueOf(myNum.getNationalNumber())); } else { final TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); final String regionCode = tm.getSimCountryIso().toUpperCase(Locale.US); CountryCode cc = new CountryCode(); cc.regionCode = regionCode; cc.countryCode = util.getCountryCodeForRegion(regionCode); mCountryCode.setSelection(ccList.getPositionForId(cc)); } // listener for autoselecting country code from typed phone number mPhone.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // unused } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // unused } @Override public void afterTextChanged(Editable s) { syncCountryCodeSelector(); } }); // configuration change?? RetainData data = (RetainData) getLastCustomNonConfigurationInstance(); if (data != null) { synchronized (this) { // sync starter was queued, we can exit if (data.syncing) { delayedSync(); } mValidator = data.validator; if (mValidator != null) mValidator.setListener(this); } if (data.progressMessage != null) { setProgressMessage(data.progressMessage, true); } } } /** Not used. */ @Override protected boolean isNormalUpNavigation() { return false; } @Override protected void onSaveInstanceState(Bundle state) { super.onSaveInstanceState(state); state.putString("name", mName); state.putString("phoneNumber", mPhoneNumber); state.putParcelable("key", mKey); state.putString("passphrase", mPassphrase); state.putByteArray("importedPrivateKey", mImportedPrivateKey); state.putByteArray("importedPublicKey", mImportedPublicKey); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mName = savedInstanceState.getString("name"); mPhoneNumber = savedInstanceState.getString("phoneNumber"); mKey = savedInstanceState.getParcelable("key"); mPassphrase = savedInstanceState.getString("passphrase"); mImportedPublicKey = savedInstanceState.getByteArray("importedPublicKey"); mImportedPrivateKey = savedInstanceState.getByteArray("importedPrivateKey"); } /** Returning the validator thread. */ @Override public Object onRetainCustomNonConfigurationInstance() { RetainData data = new RetainData(); data.validator = mValidator; if (mProgress != null) data.progressMessage = mProgressMessage; data.syncing = mSyncing; return data; } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.number_validation_menu, menu); menu.findItem(R.id.menu_manual_verification).setVisible(BuildConfig.DEBUG); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_settings: { PreferencesActivity.start(this); break; } case R.id.menu_import_key: { importKey(); break; } case R.id.menu_manual_verification: { validateCode(); break; } default: return true; } return false; } @Override protected void onStart() { super.onStart(); Preferences.RegistrationProgress saved = null; if (mClearState) { Preferences.clearRegistrationProgress(); mClearState = false; } else { try { saved = Preferences.getRegistrationProgress(); } catch (Exception e) { Log.w(TAG, "unable to restore registration progress"); Preferences.clearRegistrationProgress(); } } if (saved != null) { mName = saved.name; mPhoneNumber = saved.phone; mKey = saved.key; mPassphrase = saved.passphrase; mImportedPublicKey = saved.importedPublicKey; mImportedPrivateKey = saved.importedPrivateKey; mTrustedKeys = saved.trustedKeys; // update UI mNameText.setText(mName); mPhone.setText(mPhoneNumber); syncCountryCodeSelector(); startValidationCode(REQUEST_MANUAL_VALIDATION, saved.sender, saved.challenge, saved.server, false); } if (mKey == null) { PersonalKeyRunnable action = new PersonalKeyRunnable() { public void run(PersonalKey key) { if (key != null) { mKey = key; if (mValidator != null) // this will release the waiting lock mValidator.setKey(mKey); } // no key, key pair generation started else if (BuildConfig.DEBUG) { Toast.makeText(NumberValidation.this, R.string.msg_generating_keypair, Toast.LENGTH_LONG) .show(); } } }; // random passphrase (40 characters!!!!) mPassphrase = StringUtils.randomString(40); mKeyReceiver = new KeyGeneratorReceiver(mHandler, action); IntentFilter filter = new IntentFilter(KeyPairGeneratorService.ACTION_GENERATE); filter.addAction(KeyPairGeneratorService.ACTION_STARTED); lbm.registerReceiver(mKeyReceiver, filter); Intent i = new Intent(this, KeyPairGeneratorService.class); i.setAction(KeyPairGeneratorService.ACTION_GENERATE); startService(i); } } @Override protected void onStop() { super.onStop(); keepScreenOn(false); stopKeyReceiver(); if (mProgress != null) { if (isFinishing()) mProgress.cancel(); else mProgress.dismiss(); } } private void stopKeyReceiver() { if (mKeyReceiver != null) lbm.unregisterReceiver(mKeyReceiver); } @Override protected void onUserLeaveHint() { keepScreenOn(false); if (mProgress != null) mProgress.cancel(); } @Override @SuppressWarnings("unchecked") protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_MANUAL_VALIDATION) { if (resultCode == RESULT_OK) { Map<String, Keyring.TrustedFingerprint> trustedKeys = null; Map<String, String> keys = (HashMap) data.getSerializableExtra(PARAM_TRUSTED_KEYS); if (keys != null) { trustedKeys = Keyring.fromTrustedFingerprintMap(keys); } finishLogin(data.getStringExtra(PARAM_SERVER_URI), data.getStringExtra(PARAM_CHALLENGE), data.getByteArrayExtra(PARAM_PRIVATEKEY), data.getByteArrayExtra(PARAM_PUBLICKEY), true, trustedKeys); } else if (resultCode == RESULT_FALLBACK) { mClearState = true; startValidation(data.getBooleanExtra("force", false), true); } } } void keepScreenOn(boolean active) { if (active) getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); else getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } /** Starts the validation activity. */ public static void start(Context context) { Intent i = new Intent(context, NumberValidation.class); i.putExtra(PARAM_FROM_INTERNAL, true); context.startActivity(i); } /** Sync country code with text entered by the user, if possible. */ void syncCountryCodeSelector() { try { PhoneNumberUtil util = PhoneNumberUtil.getInstance(); CountryCode cc = (CountryCode) mCountryCode.getSelectedItem(); PhoneNumber phone = util.parse(mPhone.getText().toString(), cc != null ? cc.regionCode : null); // autoselect correct country if user entered country code too if (phone.hasCountryCode()) { CountryCode ccLookup = new CountryCode(); ccLookup.regionCode = util.getRegionCodeForNumber(phone); ccLookup.countryCode = phone.getCountryCode(); int position = ((CountryCodesAdapter) mCountryCode.getAdapter()).getPositionForId(ccLookup); if (position >= 0) { mCountryCode.setSelection(position); } } } catch (NumberParseException e) { // ignored } } private void enableControls(boolean enabled) { mValidateButton.setEnabled(enabled); mCountryCode.setEnabled(enabled); mPhone.setEnabled(enabled); } private void error(int message) { new MaterialDialog.Builder(this).content(message).positiveText(android.R.string.ok).show(); } private boolean checkInput(boolean importing) { String phoneStr; // check name first if (!importing) { mName = mNameText.getText().toString().trim(); if (mName.length() == 0) { error(R.string.msg_no_name); return false; } } String phoneInput = mPhone.getText().toString(); // if the user entered a phone number use it even when importing for backward compatibility if (!importing || !phoneInput.isEmpty()) { PhoneNumberUtil util = PhoneNumberUtil.getInstance(); CountryCode cc = (CountryCode) mCountryCode.getSelectedItem(); if (!BuildConfig.DEBUG) { PhoneNumber phone; try { phone = util.parse(phoneInput, cc.regionCode); // autoselect correct country if user entered country code too if (phone.hasCountryCode()) { CountryCode ccLookup = new CountryCode(); ccLookup.regionCode = util.getRegionCodeForNumber(phone); ccLookup.countryCode = phone.getCountryCode(); int position = ((CountryCodesAdapter) mCountryCode.getAdapter()).getPositionForId(ccLookup); if (position >= 0) { mCountryCode.setSelection(position); cc = (CountryCode) mCountryCode.getItemAtPosition(position); } } // handle special cases NumberValidator.handleSpecialCases(phone); if (!util.isValidNumberForRegion(phone, cc.regionCode) && !NumberValidator.isSpecialNumber(phone)) throw new NumberParseException(ErrorType.INVALID_COUNTRY_CODE, "invalid number for region " + cc.regionCode); } catch (NumberParseException e1) { error(R.string.msg_invalid_number); return false; } // check phone number format phoneStr = util.format(phone, PhoneNumberFormat.E164); if (!PhoneNumberUtils.isWellFormedSmsAddress(phoneStr)) { Log.i(TAG, "not a well formed SMS address"); } } else { phoneStr = String.format(Locale.US, "+%d%s", cc.countryCode, mPhone.getText().toString()); } // phone is null - invalid number if (phoneStr == null) { Toast.makeText(this, R.string.warn_invalid_number, Toast.LENGTH_SHORT).show(); return false; } Log.v(TAG, "Using phone number to register: " + phoneStr); mPhoneNumber = phoneStr; } else { // we will use the data from the imported key mName = null; mPhoneNumber = null; } return true; } void startValidation(boolean force, boolean fallback) { mForce = force; enableControls(false); if (!checkInput(false) || !startValidationNormal(null, force, fallback, false)) { enableControls(true); } } private boolean startValidationNormal(String manualServer, boolean force, boolean fallback, boolean testImport) { if (!SystemUtils.isNetworkConnectionAvailable(this)) { error(R.string.err_validation_nonetwork); return false; } // start async request Log.d(TAG, "phone number checked, sending validation request"); startProgress(testImport ? getText(R.string.msg_importing_key) : null); EndpointServer.EndpointServerProvider provider; if (manualServer != null) { provider = new EndpointServer.SingleServerProvider(manualServer); } else { provider = Preferences.getEndpointServerProvider(this); } boolean imported = (mImportedPrivateKey != null && mImportedPublicKey != null); mValidator = new NumberValidator(this, provider, mName, mPhoneNumber, imported ? null : mKey, mPassphrase); mValidator.setListener(this); mValidator.setForce(force); mValidator.setFallback(fallback); if (imported) mValidator.importKey(mImportedPrivateKey, mImportedPublicKey); if (testImport) mValidator.testImport(); mValidator.start(); return true; } /** * Begins validation of the phone number. * Also used by the view definition as the {@link OnClickListener}. * @param v not used */ public void validatePhone(View v) { keepScreenOn(true); startValidation(false, false); } /** Opens manual validation window immediately. */ public void validateCode() { if (checkInput(false)) startValidationCode(REQUEST_VALIDATION_CODE, null, null); } /** Opens import keys from another device wizard. */ private void importKey() { if (checkInput(true)) { // import keys -- number verification with server is still needed // though because of key rollback protection // TODO allow for manual validation too // do not wait for the generated key stopKeyReceiver(); new FileChooserDialog.Builder(NumberValidation.this) .initialPath(PersonalKeyPack.DEFAULT_KEYPACK.getParent()).mimeType(PersonalKeyPack.KEYPACK_MIME) .show(); } } private void importAskPassphrase(final ZipInputStream zip) { new MaterialDialog.Builder(this).title(R.string.title_passphrase) .inputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) .input(null, null, new MaterialDialog.InputCallback() { @Override public void onInput(MaterialDialog dialog, CharSequence input) { startImport(zip, dialog.getInputEditText().getText().toString()); } }).onNegative(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) { try { zip.close(); } catch (IOException e) { // ignored } } }).negativeText(android.R.string.cancel).positiveText(android.R.string.ok).show(); } private void startImport(InputStream in) { ZipInputStream zip = null; try { zip = new ZipInputStream(in); // ask passphrase to user and assign to mPassphrase importAskPassphrase(zip); } catch (Exception e) { Log.e(TAG, "error importing keys", e); ReportingManager.logException(e); mImportedPublicKey = mImportedPrivateKey = null; mTrustedKeys = null; try { if (zip != null) zip.close(); } catch (IOException ignored) { // ignored. } Toast.makeText(NumberValidation.this, R.string.err_import_keypair_failed, Toast.LENGTH_LONG).show(); } } @Override public void onFileSelection(@NonNull FileChooserDialog fileChooserDialog, @NonNull File file) { try { startImport(new FileInputStream(file)); } catch (FileNotFoundException e) { Log.e(TAG, "error importing keys", e); Toast.makeText(this, R.string.err_import_keypair_read, Toast.LENGTH_LONG).show(); } } void startImport(ZipInputStream zip, String passphrase) { PersonalKeyImporter importer = null; String manualServer = Preferences.getServerURI(); try { importer = new PersonalKeyImporter(zip, passphrase); importer.load(); // we do not save this test key into the mKey field // we need it to be clear so the validator will use the imported data // createPersonalKey is called only to make sure data is valid PersonalKey key = importer.createPersonalKey(); if (key == null) throw new PGPException("unable to load imported personal key."); String uidStr = key.getUserId(null); PGPUserID uid = PGPUserID.parse(uidStr); if (uid == null) throw new PGPException("malformed user ID: " + uidStr); Map<String, String> accountInfo = importer.getAccountInfo(); if (accountInfo != null) { String phoneNumber = accountInfo.get("phoneNumber"); if (!TextUtils.isEmpty(phoneNumber)) { mPhoneNumber = phoneNumber; } } if (mPhoneNumber == null) { Toast.makeText(this, R.string.warn_invalid_number, Toast.LENGTH_SHORT).show(); return; } // check that uid matches phone number String email = uid.getEmail(); String numberHash = MessageUtils.sha1(mPhoneNumber); String localpart = XmppStringUtils.parseLocalpart(email); if (!numberHash.equalsIgnoreCase(localpart)) throw new PGPUidMismatchException("email does not match phone number: " + email); // use server from the key only if we didn't set our own if (TextUtils.isEmpty(manualServer)) manualServer = XmppStringUtils.parseDomain(email); mName = uid.getName(); mImportedPublicKey = importer.getPublicKeyData(); mImportedPrivateKey = importer.getPrivateKeyData(); try { mTrustedKeys = importer.getTrustedKeys(); } catch (Exception e) { // this is not a critical error so we can just ignore it Log.w(TAG, "unable to load trusted keys from key pack", e); ReportingManager.logException(e); } } catch (PGPUidMismatchException e) { Log.w(TAG, "uid mismatch!"); mImportedPublicKey = mImportedPrivateKey = null; mName = null; Toast.makeText(this, R.string.err_import_keypair_uid_mismatch, Toast.LENGTH_LONG).show(); } catch (Exception e) { Log.e(TAG, "error importing keys", e); ReportingManager.logException(e); mImportedPublicKey = mImportedPrivateKey = null; mTrustedKeys = null; mName = null; Toast.makeText(this, R.string.err_import_keypair_failed, Toast.LENGTH_LONG).show(); } finally { try { if (importer != null) importer.close(); } catch (Exception e) { // ignored } } if (mImportedPublicKey != null && mImportedPrivateKey != null) { // we can now store the passphrase mPassphrase = passphrase; // begin usual validation // TODO implement fallback usage if (!startValidationNormal(manualServer, true, false, true)) { enableControls(true); } } } /** No search here. */ @Override public boolean onSearchRequested() { return false; } public void startProgress() { startProgress(null); } private void startProgress(CharSequence message) { if (mProgress == null) { mProgress = new NonSearchableDialog.Builder(this).progress(true, 0) .cancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { keepScreenOn(false); Toast.makeText(NumberValidation.this, R.string.msg_validation_canceled, Toast.LENGTH_LONG).show(); abort(); } }).dismissListener(new OnDismissListener() { public void onDismiss(DialogInterface dialog) { // remove sync starter if (mSyncStart != null) { mHandler.removeCallbacks(mSyncStart); mSyncStart = null; } } }).build(); mProgress.setCanceledOnTouchOutside(false); setProgressMessage(message != null ? message : getText(R.string.msg_validating_phone)); } mProgress.show(); } public void abortProgress() { if (mProgress != null) { mProgress.dismiss(); mProgress = null; } } public void abortProgress(boolean enableControls) { abortProgress(); enableControls(enableControls); } public void abort() { abort(false); } public void abort(boolean ending) { if (!ending) { abortProgress(true); } mForce = false; if (mValidator != null) { mValidator.shutdown(); mValidator = null; } } private void setProgressMessage(CharSequence message) { setProgressMessage(message, false); } private void setProgressMessage(CharSequence message, boolean create) { if (mProgress == null && create) { startProgress(message); } if (mProgress != null) { mProgressMessage = message; mProgress.setContent(message); } } void delayedSync() { mSyncing = true; mSyncStart = new Runnable() { public void run() { // start has been requested mSyncStart = null; // enable services Kontalk.setServicesEnabled(NumberValidation.this, true); // start sync SyncAdapter.requestSync(NumberValidation.this, true); // if we have been called internally, start ConversationList if (mFromInternal) startActivity(new Intent(getApplicationContext(), ConversationsActivity.class)); Toast.makeText(getApplicationContext(), R.string.msg_authenticated, Toast.LENGTH_LONG).show(); // end this abortProgress(); finish(); } }; /* * This is a workaround for API level... I don't know since when :D * Seems that requesting sync too soon after account creation has no * effect. We delay sync by some time to give it time to settle. */ mHandler.postDelayed(mSyncStart, 2000); } /** Used only if imported key was tested successfully. */ @Override public void onAuthTokenReceived(final NumberValidator v, final byte[] privateKey, final byte[] publicKey) { runOnUiThread(new Runnable() { @Override public void run() { abort(true); finishLogin(v.getServer().toString(), v.getServerChallenge(), privateKey, publicKey, false, mTrustedKeys); } }); } @Override public void onAuthTokenFailed(NumberValidator v, int reason) { Log.e(TAG, "authentication token request failed (" + reason + ")"); runOnUiThread(new Runnable() { @Override public void run() { keepScreenOn(false); Toast.makeText(NumberValidation.this, R.string.err_authentication_failed, Toast.LENGTH_LONG).show(); abort(); } }); } private void statusInitializing() { if (mProgress == null) startProgress(); mProgress.setCancelable(false); setProgressMessage(getString(R.string.msg_initializing)); } protected void finishLogin(final String serverUri, final String challenge, final byte[] privateKeyData, final byte[] publicKeyData, boolean updateKey, Map<String, Keyring.TrustedFingerprint> trustedKeys) { Log.v(TAG, "finishing login"); statusInitializing(); if (updateKey) { // update public key try { mKey.update(publicKeyData); } catch (IOException e) { // abort throw new RuntimeException("error decoding public key", e); } } completeLogin(serverUri, challenge, privateKeyData, publicKeyData, trustedKeys); } private void completeLogin(String serverUri, String challenge, byte[] privateKeyData, byte[] publicKeyData, Map<String, Keyring.TrustedFingerprint> trustedKeys) { // generate the bridge certificate byte[] bridgeCertData; try { bridgeCertData = X509Bridge.createCertificate(privateKeyData, publicKeyData, mPassphrase).getEncoded(); } catch (Exception e) { // abort throw new RuntimeException("unable to build X.509 bridge certificate", e); } final Account account = new Account(mPhoneNumber, Authenticator.ACCOUNT_TYPE); // workaround for bug in AccountManager (http://stackoverflow.com/a/11698139/1045199) // procedure will continue in removeAccount callback mAccountManager.removeAccount(account, new AccountRemovalCallback(this, account, mPassphrase, privateKeyData, publicKeyData, bridgeCertData, mName, serverUri, challenge, trustedKeys), mHandler); } @Override public void onError(NumberValidator v, final Throwable e) { Log.e(TAG, "validation error.", e); runOnUiThread(new Runnable() { @Override public void run() { keepScreenOn(false); int msgId; if (e instanceof SocketException) msgId = R.string.err_validation_network_error; else msgId = R.string.err_validation_error; Toast.makeText(NumberValidation.this, msgId, Toast.LENGTH_LONG).show(); abort(); } }); } @Override public void onServerCheckFailed(NumberValidator v) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(NumberValidation.this, R.string.err_validation_server_not_supported, Toast.LENGTH_LONG).show(); abort(); } }); } @Override public void onValidationFailed(NumberValidator v, final int reason) { Log.e(TAG, "phone number validation failed (" + reason + ")"); runOnUiThread(new Runnable() { @Override public void run() { if (reason == NumberValidator.ERROR_USER_EXISTS) { userExistsWarning(); } else { int msg; if (reason == NumberValidator.ERROR_THROTTLING) msg = R.string.err_validation_retry_later; else msg = R.string.err_validation_failed; Toast.makeText(NumberValidation.this, msg, Toast.LENGTH_LONG).show(); } abort(); } }); } @Override public void onValidationRequested(NumberValidator v, String sender, String challenge) { Log.d(TAG, "validation has been requested, requesting validation code to user"); proceedManual(sender, challenge); } void userExistsWarning() { MaterialDialog.Builder builder = new MaterialDialog.Builder(this) .content(R.string.err_validation_user_exists).positiveText(android.R.string.ok) .positiveColorRes(R.color.button_danger).negativeText(android.R.string.cancel) .neutralText(R.string.learn_more).onAny(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { switch (which) { case POSITIVE: startValidation(true, false); break; case NEUTRAL: SystemUtils.openURL(NumberValidation.this, getString(R.string.help_import_key)); break; } } }); try { builder.show(); } catch (MaterialDialog.DialogException ignored) { } } /** Proceeds to the next step in manual validation. */ private void proceedManual(final String sender, final String challenge) { runOnUiThread(new Runnable() { @Override public void run() { abortProgress(true); startValidationCode(REQUEST_MANUAL_VALIDATION, sender, challenge); } }); } void startValidationCode(int requestCode, String sender, String challenge) { startValidationCode(requestCode, sender, challenge, null, true); } private void startValidationCode(int requestCode, String sender, String challenge, EndpointServer server, boolean saveProgress) { // validator might be null if we are skipping verification code request String serverUri = null; if (server != null) serverUri = server.toString(); else if (mValidator != null) serverUri = mValidator.getServer().toString(); // save state to preferences if (saveProgress) { Preferences.saveRegistrationProgress(mName, mPhoneNumber, mKey, mPassphrase, mImportedPublicKey, mImportedPrivateKey, serverUri, sender, challenge, mForce, mTrustedKeys); } Intent i = new Intent(NumberValidation.this, CodeValidation.class); i.putExtra("requestCode", requestCode); i.putExtra("name", mName); i.putExtra("phone", mPhoneNumber); i.putExtra("force", mForce); i.putExtra("passphrase", mPassphrase); i.putExtra("importedPublicKey", mImportedPublicKey); i.putExtra("importedPrivateKey", mImportedPrivateKey); i.putExtra("trustedKeys", mTrustedKeys != null ? (HashMap) Keyring.toTrustedFingerprintMap(mTrustedKeys) : null); i.putExtra("server", serverUri); i.putExtra("sender", sender); i.putExtra("challenge", challenge); i.putExtra(KeyPairGeneratorService.EXTRA_KEY, mKey); startActivityForResult(i, REQUEST_MANUAL_VALIDATION); } private static class AccountRemovalCallback implements AccountManagerCallback<Boolean> { private WeakReference<NumberValidation> a; private final Account account; private final String passphrase; private final byte[] privateKeyData; private final byte[] publicKeyData; private final byte[] bridgeCertData; private final String name; private final String serverUri; private final String challenge; private final Map<String, Keyring.TrustedFingerprint> trustedKeys; AccountRemovalCallback(NumberValidation activity, Account account, String passphrase, byte[] privateKeyData, byte[] publicKeyData, byte[] bridgeCertData, String name, String serverUri, String challenge, Map<String, Keyring.TrustedFingerprint> trustedKeys) { this.a = new WeakReference<>(activity); this.account = account; this.passphrase = passphrase; this.privateKeyData = privateKeyData; this.publicKeyData = publicKeyData; this.bridgeCertData = bridgeCertData; this.name = name; this.serverUri = serverUri; this.challenge = challenge; this.trustedKeys = trustedKeys; } @Override public void run(AccountManagerFuture<Boolean> result) { NumberValidation ctx = a.get(); if (ctx != null) { // store trusted keys if (trustedKeys != null) { Keyring.setTrustedKeys(ctx, trustedKeys); } AccountManager am = (AccountManager) ctx.getSystemService(Context.ACCOUNT_SERVICE); // account userdata Bundle data = new Bundle(); data.putString(Authenticator.DATA_PRIVATEKEY, Base64.encodeToString(privateKeyData, Base64.NO_WRAP)); data.putString(Authenticator.DATA_PUBLICKEY, Base64.encodeToString(publicKeyData, Base64.NO_WRAP)); data.putString(Authenticator.DATA_BRIDGECERT, Base64.encodeToString(bridgeCertData, Base64.NO_WRAP)); data.putString(Authenticator.DATA_NAME, name); data.putString(Authenticator.DATA_SERVER_URI, serverUri); // this is the password to the private key am.addAccountExplicitly(account, passphrase, data); // put data once more (workaround for Android bug http://stackoverflow.com/a/11698139/1045199) am.setUserData(account, Authenticator.DATA_PRIVATEKEY, data.getString(Authenticator.DATA_PRIVATEKEY)); am.setUserData(account, Authenticator.DATA_PUBLICKEY, data.getString(Authenticator.DATA_PUBLICKEY)); am.setUserData(account, Authenticator.DATA_BRIDGECERT, data.getString(Authenticator.DATA_BRIDGECERT)); am.setUserData(account, Authenticator.DATA_NAME, data.getString(Authenticator.DATA_NAME)); am.setUserData(account, Authenticator.DATA_SERVER_URI, serverUri); // Set contacts sync for this account. ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); // send back result final Intent intent = new Intent(); intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, account.name); intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Authenticator.ACCOUNT_TYPE); ctx.setAccountAuthenticatorResult(intent.getExtras()); ctx.setResult(RESULT_OK, intent); ReportingManager.logSignUp(challenge); // manual sync starter ctx.delayedSync(); } } } }