diff options
Diffstat (limited to 'src/main/java/eu')
10 files changed, 436 insertions, 212 deletions
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 3077c4889..c73a05647 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -577,6 +577,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void writeRoster(final Roster roster) { final Account account = roster.getAccount(); final SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransaction(); for (Contact contact : roster.getContacts()) { if (contact.getOption(Contact.Options.IN_ROSTER)) { db.insert(Contact.TABLENAME, null, contact.getContentValues()); @@ -586,6 +587,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.delete(Contact.TABLENAME, where, whereArgs); } } + db.setTransactionSuccessful(); + db.endTransaction(); account.setRosterVersion(roster.getVersion()); updateAccount(account); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 4f9011366..4e57d7f81 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.ui; -import android.Manifest; import android.annotation.SuppressLint; import android.app.ActionBar; import android.app.AlertDialog; @@ -18,6 +17,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; +import android.provider.Settings; import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; import android.util.Log; @@ -38,6 +38,8 @@ import android.widget.Toast; import net.java.otr4j.session.SessionStatus; +import org.openintents.openpgp.util.OpenPgpApi; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -66,7 +68,6 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -import org.openintents.openpgp.util.OpenPgpApi; public class ConversationActivity extends XmppActivity implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast { @@ -207,7 +208,7 @@ public class ConversationActivity extends XmppActivity @Override public void onItemClick(AdapterView<?> arg0, View clickedView, - int position, long arg3) { + int position, long arg3) { if (getSelectedConversation() != conversationList.get(position)) { setSelectedConversation(conversationList.get(position)); ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation()); @@ -296,7 +297,7 @@ public class ConversationActivity extends XmppActivity SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; mSlidingPaneLayout.setParallaxDistance(150); mSlidingPaneLayout - .setShadowResource(R.drawable.es_slidingpane_shadow); + .setShadowResource(R.drawable.es_slidingpane_shadow); mSlidingPaneLayout.setSliderFadeColor(0); mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() { @@ -307,7 +308,7 @@ public class ConversationActivity extends XmppActivity hideKeyboard(); if (xmppConnectionServiceBound) { xmppConnectionService.getNotificationService() - .setOpenConversation(null); + .setOpenConversation(null); } closeContextMenu(); } @@ -504,7 +505,7 @@ public class ConversationActivity extends XmppActivity case ATTACHMENT_CHOICE_CHOOSE_IMAGE: intent.setAction(Intent.ACTION_GET_CONTENT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE,true); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } intent.setType("image/*"); chooser = true; @@ -571,16 +572,16 @@ public class ConversationActivity extends XmppActivity } switch (attachmentChoice) { case ATTACHMENT_CHOICE_LOCATION: - getPreferences().edit().putString("recently_used_quick_action","location").apply(); + getPreferences().edit().putString("recently_used_quick_action", "location").apply(); break; case ATTACHMENT_CHOICE_RECORD_VOICE: - getPreferences().edit().putString("recently_used_quick_action","voice").apply(); + getPreferences().edit().putString("recently_used_quick_action", "voice").apply(); break; case ATTACHMENT_CHOICE_TAKE_PHOTO: - getPreferences().edit().putString("recently_used_quick_action","photo").apply(); + getPreferences().edit().putString("recently_used_quick_action", "photo").apply(); break; case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - getPreferences().edit().putString("recently_used_quick_action","picture").apply(); + getPreferences().edit().putString("recently_used_quick_action", "picture").apply(); break; } final Conversation conversation = getSelectedConversation(); @@ -620,19 +621,19 @@ public class ConversationActivity extends XmppActivity selectPresenceToAttachFile(attachmentChoice, encryption); } else { final ConversationFragment fragment = (ConversationFragment) getFragmentManager() - .findFragmentByTag("conversation"); + .findFragmentByTag("conversation"); if (fragment != null) { fragment.showNoPGPKeyDialog(false, new OnClickListener() { @Override public void onClick(DialogInterface dialog, - int which) { + int which) { conversation - .setNextEncryption(Message.ENCRYPTION_NONE); + .setNextEncryption(Message.ENCRYPTION_NONE); xmppConnectionService.databaseBackend - .updateConversation(conversation); - selectPresenceToAttachFile(attachmentChoice,Message.ENCRYPTION_NONE); + .updateConversation(conversation); + selectPresenceToAttachFile(attachmentChoice, Message.ENCRYPTION_NONE); } }); } @@ -647,19 +648,6 @@ public class ConversationActivity extends XmppActivity } } - public boolean hasStoragePermission(int attachmentChoice) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, attachmentChoice); - return false; - } else { - return true; - } - } else { - return true; - } - } - @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { if (grantResults.length > 0) @@ -672,8 +660,8 @@ public class ConversationActivity extends XmppActivity attachFile(requestCode); } } else { - Toast.makeText(this,R.string.no_storage_permission,Toast.LENGTH_SHORT).show(); - } + Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); + } } public void startDownloadable(Message message) { @@ -684,14 +672,14 @@ public class ConversationActivity extends XmppActivity Transferable transferable = message.getTransferable(); if (transferable != null) { if (!transferable.start()) { - Toast.makeText(this, R.string.not_connected_try_again,Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); } } else if (message.treatAsDownloadable() != Message.Decision.NEVER) { xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true); } } - @Override + @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { showConversationsOverview(); @@ -762,9 +750,9 @@ public class ConversationActivity extends XmppActivity this.mConversationFragment.reInit(getSelectedConversation()); } else { setSelectedConversation(null); - if (mRedirected.compareAndSet(false,true)) { + if (mRedirected.compareAndSet(false, true)) { Intent intent = new Intent(this, StartConversationActivity.class); - intent.putExtra("init",true); + intent.putExtra("init", true); startActivity(intent); finish(); } @@ -779,7 +767,7 @@ public class ConversationActivity extends XmppActivity View dialogView = getLayoutInflater().inflate( R.layout.dialog_clear_history, null); final CheckBox endConversationCheckBox = (CheckBox) dialogView - .findViewById(R.id.end_conversation_checkbox); + .findViewById(R.id.end_conversation_checkbox); builder.setView(dialogView); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.delete_messages), @@ -881,7 +869,7 @@ public class ConversationActivity extends XmppActivity } PopupMenu popup = new PopupMenu(this, menuItemView); final ConversationFragment fragment = (ConversationFragment) getFragmentManager() - .findFragmentByTag("conversation"); + .findFragmentByTag("conversation"); if (fragment != null) { popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { @@ -902,7 +890,7 @@ public class ConversationActivity extends XmppActivity conversation.setNextEncryption(Message.ENCRYPTION_PGP); item.setChecked(true); } else { - announcePgp(conversation.getAccount(),conversation); + announcePgp(conversation.getAccount(), conversation); } } else { showInstallPgpDialog(); @@ -1008,7 +996,7 @@ public class ConversationActivity extends XmppActivity int rotation = getWindowManager().getDefaultDisplay().getRotation(); final int upKey; final int downKey; - switch(rotation) { + switch (rotation) { case Surface.ROTATION_90: upKey = KeyEvent.KEYCODE_DPAD_LEFT; downKey = KeyEvent.KEYCODE_DPAD_RIGHT; @@ -1032,7 +1020,7 @@ public class ConversationActivity extends XmppActivity } else if (modifier && key == downKey) { if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) { showConversationsOverview(); - } + } return selectDownConversation(); } else if (modifier && key == upKey) { if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) { @@ -1169,11 +1157,11 @@ public class ConversationActivity extends XmppActivity public void onSaveInstanceState(final Bundle savedInstanceState) { Conversation conversation = getSelectedConversation(); if (conversation != null) { - savedInstanceState.putString(STATE_OPEN_CONVERSATION,conversation.getUuid()); + savedInstanceState.putString(STATE_OPEN_CONVERSATION, conversation.getUuid()); } else { savedInstanceState.remove(STATE_OPEN_CONVERSATION); } - savedInstanceState.putBoolean(STATE_PANEL_OPEN,isConversationsOverviewVisable()); + savedInstanceState.putBoolean(STATE_PANEL_OPEN, isConversationsOverviewVisable()); if (this.mPendingImageUris.size() >= 1) { savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUris.get(0).toString()); } else { @@ -1200,7 +1188,7 @@ public class ConversationActivity extends XmppActivity } if (xmppConnectionService.getAccounts().size() == 0) { - if (mRedirected.compareAndSet(false,true)) { + if (mRedirected.compareAndSet(false, true)) { if (Config.X509_VERIFICATION) { startActivity(new Intent(this, ManageAccountActivity.class)); } else { @@ -1209,9 +1197,9 @@ public class ConversationActivity extends XmppActivity finish(); } } else if (conversationList.size() <= 0) { - if (mRedirected.compareAndSet(false,true)) { + if (mRedirected.compareAndSet(false, true)) { Intent intent = new Intent(this, StartConversationActivity.class); - intent.putExtra("init",true); + intent.putExtra("init", true); startActivity(intent); finish(); } @@ -1243,7 +1231,7 @@ public class ConversationActivity extends XmppActivity this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); } - if(!forbidProcessingPendings) { + if (!forbidProcessingPendings) { for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { Uri foo = i.next(); attachImageToConversation(getSelectedConversation(), foo); @@ -1260,7 +1248,9 @@ public class ConversationActivity extends XmppActivity } forbidProcessingPendings = false; - ExceptionHelper.checkForCrash(this, this.xmppConnectionService); + if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) { + openBatteryOptimizationDialogIfNeeded(); + } setIntent(new Intent()); } @@ -1269,14 +1259,14 @@ public class ConversationActivity extends XmppActivity final String downloadUuid = intent.getStringExtra(MESSAGE); final String text = intent.getStringExtra(TEXT); final String nick = intent.getStringExtra(NICK); - final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE,false); + final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE, false); if (selectConversationByUuid(uuid)) { this.mConversationFragment.reInit(getSelectedConversation()); if (nick != null) { if (pm) { Jid jid = getSelectedConversation().getJid(); try { - Jid next = Jid.fromParts(jid.getLocalpart(),jid.getDomainpart(),nick); + Jid next = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), nick); this.mConversationFragment.privateMessageWith(next); } catch (final InvalidJidException ignored) { //do nothing @@ -1326,7 +1316,7 @@ public class ConversationActivity extends XmppActivity Uri uri = intent.getData(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) { ClipData clipData = intent.getClipData(); - for(int i = 0; i < clipData.getItemCount(); ++i) { + for (int i = 0; i < clipData.getItemCount(); ++i) { uris.add(clipData.getItemAt(i).getUri()); } } else { @@ -1336,8 +1326,7 @@ public class ConversationActivity extends XmppActivity } @Override - protected void onActivityResult(int requestCode, int resultCode, - final Intent data) { + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == REQUEST_DECRYPT_PGP) { @@ -1365,15 +1354,15 @@ public class ConversationActivity extends XmppActivity mPendingImageUris.clear(); mPendingImageUris.addAll(extractUriFromIntent(data)); if (xmppConnectionServiceBound) { - for(Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { - attachImageToConversation(getSelectedConversation(),i.next()); + for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { + attachImageToConversation(getSelectedConversation(), i.next()); } } } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) { mPendingFileUris.clear(); mPendingFileUris.addAll(extractUriFromIntent(data)); if (xmppConnectionServiceBound) { - for(Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { + for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { attachFileToConversation(getSelectedConversation(), i.next()); } } @@ -1391,9 +1380,9 @@ public class ConversationActivity extends XmppActivity mPendingImageUris.clear(); } } else if (requestCode == ATTACHMENT_CHOICE_LOCATION) { - double latitude = data.getDoubleExtra("latitude",0); - double longitude = data.getDoubleExtra("longitude",0); - this.mPendingGeoUri = Uri.parse("geo:"+String.valueOf(latitude)+","+String.valueOf(longitude)); + double latitude = data.getDoubleExtra("latitude", 0); + double longitude = data.getDoubleExtra("longitude", 0); + this.mPendingGeoUri = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude)); if (xmppConnectionServiceBound) { attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); this.mPendingGeoUri = null; @@ -1408,6 +1397,39 @@ public class ConversationActivity extends XmppActivity if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) { mConversationFragment.onActivityResult(requestCode, resultCode, data); } + if (requestCode == REQUEST_BATTERY_OP) { + setNeverAskForBatteryOptimizationsAgain(); + } + } + } + + private void setNeverAskForBatteryOptimizationsAgain() { + getPreferences().edit().putBoolean("show_battery_optimization", false).commit(); + } + + private void openBatteryOptimizationDialogIfNeeded() { + if (showBatteryOptimizationWarning() && getPreferences().getBoolean("show_battery_optimizationF", true)) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.battery_optimizations_enabled); + builder.setMessage(R.string.battery_optimizations_enabled_dialog); + builder.setPositiveButton(R.string.next, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + Uri uri = Uri.parse("package:" + getPackageName()); + intent.setData(uri); + startActivityForResult(intent, REQUEST_BATTERY_OP); + } + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + builder.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + setNeverAskForBatteryOptimizationsAgain(); + } + }); + } + builder.create().show(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 1545de0f3..22b875c2e 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -5,9 +5,10 @@ import android.app.AlertDialog.Builder; import android.app.PendingIntent; import android.content.DialogInterface; import android.content.Intent; -import android.content.res.Configuration; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; import android.security.KeyChain; import android.security.KeyChainAliasCallback; import android.text.Editable; @@ -52,16 +53,17 @@ import eu.siacs.conversations.xmpp.pep.Avatar; public class EditAccountActivity extends XmppActivity implements OnAccountUpdate, OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast { - private LinearLayout mMainLayout; private AutoCompleteTextView mAccountJid; private EditText mPassword; private EditText mPasswordConfirm; private CheckBox mRegisterNew; private Button mCancelButton; private Button mSaveButton; + private Button mDisableBatterOptimizations; private TableLayout mMoreTable; private LinearLayout mStats; + private RelativeLayout mBatteryOptimizations; private TextView mServerInfoSm; private TextView mServerInfoRosterVersion; private TextView mServerInfoCarbons; @@ -311,6 +313,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate }); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_BATTERY_OP) { + updateAccountInformation(mAccount == null); + } + } + protected void updateSaveButton() { if (accountInfoEdited() && !mInitMode) { this.mSaveButton.setText(R.string.save); @@ -372,7 +382,6 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit_account); - this.mMainLayout = (LinearLayout) findViewById(R.id.account_main_layout); this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid); this.mAccountJid.addTextChangedListener(this.mTextWatcher); this.mAccountJidLabel = (TextView) findViewById(R.id.account_jid_label); @@ -387,6 +396,17 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mAvatar.setOnClickListener(this.mAvatarClickListener); this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new); this.mStats = (LinearLayout) findViewById(R.id.stats); + this.mBatteryOptimizations = (RelativeLayout) findViewById(R.id.battery_optimization); + this.mDisableBatterOptimizations = (Button) findViewById(R.id.batt_op_disable); + this.mDisableBatterOptimizations.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + Uri uri = Uri.parse("package:"+getPackageName()); + intent.setData(uri); + startActivityForResult(intent,REQUEST_BATTERY_OP); + } + }); this.mSessionEst = (TextView) findViewById(R.id.session_est); this.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version); this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); @@ -595,6 +615,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { this.mStats.setVisibility(View.VISIBLE); + this.mBatteryOptimizations.setVisibility(showBatteryOptimizationWarning() ? View.VISIBLE : View.GONE); this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() .getLastSessionEstablished())); Features features = this.mAccount.getXmppConnection().getFeatures(); diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 3313ce318..edd6cf150 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.ui; import android.app.PendingIntent; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; @@ -137,16 +138,35 @@ public class PublishProfilePictureActivity extends XmppActivity { @Override public void onClick(View v) { - Intent attachFileIntent = new Intent(); - attachFileIntent.setType("image/*"); - attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); - Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); - startActivityForResult(chooser, REQUEST_CHOOSE_FILE); + if (hasStoragePermission(REQUEST_CHOOSE_FILE)) { + chooseAvatar(); + } + } }); this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext()); } + private void chooseAvatar() { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_CHOOSE_FILE); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + if (grantResults.length > 0) + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (requestCode == REQUEST_CHOOSE_FILE) { + chooseAvatar(); + } + } else { + Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); + } + } + @Override protected void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index 2e6d32466..bc319108c 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -42,6 +42,8 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate private Button mSaveButton; private Button mCancelButton; + private AxolotlService.FetchStatus lastFetchReport = AxolotlService.FetchStatus.SUCCESS; + private final Map<String, Boolean> ownKeysToTrust = new HashMap<>(); private final Map<String, Boolean> foreignKeysToTrust = new HashMap<>(); @@ -160,7 +162,11 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate } else { if (!hasForeignKeys && hasNoOtherTrustedKeys()) { keyErrorMessageCard.setVisibility(View.VISIBLE); - keyErrorMessage.setText(R.string.error_no_keys_to_trust); + if (lastFetchReport == AxolotlService.FetchStatus.ERROR) { + keyErrorMessage.setText(R.string.error_no_keys_to_trust_server_error); + } else { + keyErrorMessage.setText(R.string.error_no_keys_to_trust); + } ownKeys.removeAllViews(); ownKeysCard.setVisibility(View.GONE); foreignKeys.removeAllViews(); foreignKeysCard.setVisibility(View.GONE); } @@ -216,6 +222,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate @Override public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) { if (report != null) { + lastFetchReport = report; runOnUiThread(new Runnable() { @Override public void run() { diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index be6255127..85077c9df 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.ui; +import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.ActionBar; @@ -35,6 +36,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.PowerManager; import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.InputType; @@ -91,6 +93,7 @@ public abstract class XmppActivity extends Activity { protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103; + protected static final int REQUEST_BATTERY_OP = 0x13849ff; public XmppConnectionService xmppConnectionService; public boolean xmppConnectionServiceBound = false; @@ -375,6 +378,15 @@ public abstract class XmppActivity extends Activity { } } + protected boolean showBatteryOptimizationWarning() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + return !pm.isIgnoringBatteryOptimizations(getPackageName()); + } else { + return false; + } + } + protected boolean usingEnterKey() { return getPreferences().getBoolean("display_enter_key", false); } @@ -785,6 +797,19 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } + public boolean hasStoragePermission(int requestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode); + return false; + } else { + return true; + } + } else { + return true; + } + } + public void selectPresence(final Conversation conversation, final OnPresenceSelected listener) { final Contact contact = conversation.getContact(); diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index e07df627e..87790d64d 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; import java.util.Random; import java.util.TreeMap; +import java.util.Map; import java.util.regex.Pattern; import de.measite.minidns.Client; @@ -57,7 +58,7 @@ public class DNSHelper { if (!b.containsKey("values")) { Log.d(Config.LOGTAG,"all dns queries failed. provide fallback A record"); ArrayList<Parcelable> values = new ArrayList<>(); - values.add(createNamePortBundle(host,5222)); + values.add(createNamePortBundle(host, 5222, false)); b.putParcelableArrayList("values",values); } return b; @@ -96,57 +97,73 @@ public class DNSHelper { return servers; } - public static Bundle queryDNS(String host, InetAddress dnsServer) { - Bundle bundle = new Bundle(); - try { - client.setTimeout(Config.PING_TIMEOUT * 1000); - String qname = "_xmpp-client._tcp." + host; - Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host); - DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServer.getHostAddress()); - - TreeMap<Integer, ArrayList<SRV>> priorities = new TreeMap<>(); - TreeMap<String, ArrayList<String>> ips4 = new TreeMap<>(); - TreeMap<String, ArrayList<String>> ips6 = new TreeMap<>(); - - for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) { - for (Record rr : rrset) { - Data d = rr.getPayload(); - if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { - SRV srv = (SRV) d; - if (!priorities.containsKey(srv.getPriority())) { - priorities.put(srv.getPriority(),new ArrayList<SRV>()); - } - priorities.get(srv.getPriority()).add(srv); + private static class TlsSrv { + private final SRV srv; + private final boolean tls; + + public TlsSrv(SRV srv, boolean tls) { + this.srv = srv; + this.tls = tls; + } + } + + private static void fillSrvMaps(final String qname, final InetAddress dnsServer, final Map<Integer, List<TlsSrv>> priorities, final Map<String, List<String>> ips4, final Map<String, List<String>> ips6, final boolean tls) throws IOException { + final DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServer.getHostAddress()); + for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) { + for (Record rr : rrset) { + Data d = rr.getPayload(); + if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { + SRV srv = (SRV) d; + if (!priorities.containsKey(srv.getPriority())) { + priorities.put(srv.getPriority(),new ArrayList<TlsSrv>()); } - if (d instanceof A) { - A a = (A) d; - if (!ips4.containsKey(rr.getName())) { - ips4.put(rr.getName(), new ArrayList<String>()); - } - ips4.get(rr.getName()).add(a.toString()); + priorities.get(srv.getPriority()).add(new TlsSrv(srv, tls)); + } + if (d instanceof A) { + A a = (A) d; + if (!ips4.containsKey(rr.getName())) { + ips4.put(rr.getName(), new ArrayList<String>()); } - if (d instanceof AAAA) { - AAAA aaaa = (AAAA) d; - if (!ips6.containsKey(rr.getName())) { - ips6.put(rr.getName(), new ArrayList<String>()); - } - ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); + ips4.get(rr.getName()).add(a.toString()); + } + if (d instanceof AAAA) { + AAAA aaaa = (AAAA) d; + if (!ips6.containsKey(rr.getName())) { + ips6.put(rr.getName(), new ArrayList<String>()); } + ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); } } + } + } + + public static Bundle queryDNS(String host, InetAddress dnsServer) { + Bundle bundle = new Bundle(); + try { + client.setTimeout(Config.PING_TIMEOUT * 1000); + final String qname = "_xmpp-client._tcp." + host; + final String tlsQname = "_xmpps-client._tcp." + host; + Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host); + + final Map<Integer, List<TlsSrv>> priorities = new TreeMap<>(); + final Map<String, List<String>> ips4 = new TreeMap<>(); + final Map<String, List<String>> ips6 = new TreeMap<>(); + + fillSrvMaps(qname, dnsServer, priorities, ips4, ips6, false); + fillSrvMaps(tlsQname, dnsServer, priorities, ips4, ips6, true); - ArrayList<SRV> result = new ArrayList<>(); - for (ArrayList<SRV> s : priorities.values()) { + final List<TlsSrv> result = new ArrayList<>(); + for (final List<TlsSrv> s : priorities.values()) { result.addAll(s); } - ArrayList<Bundle> values = new ArrayList<>(); + final ArrayList<Bundle> values = new ArrayList<>(); if (result.size() == 0) { DNSMessage response; try { response = client.query(host, TYPE.A, CLASS.IN, dnsServer.getHostAddress()); for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload())); + values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload(), false)); } } catch (SocketTimeoutException e) { Log.d(Config.LOGTAG,"ignoring timeout exception when querying A record on "+dnsServer.getHostAddress()); @@ -154,37 +171,38 @@ public class DNSHelper { try { response = client.query(host, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload())); + values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload(), false)); } } catch (SocketTimeoutException e) { Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress()); } - values.add(createNamePortBundle(host,5222)); + values.add(createNamePortBundle(host, 5222, false)); bundle.putParcelableArrayList("values", values); return bundle; } - for (SRV srv : result) { + for (final TlsSrv tlsSrv : result) { + final SRV srv = tlsSrv.srv; if (ips6.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6)); + values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6, tlsSrv.tls)); } else { try { DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(srv.getName(), srv.getPort(), response.getAnswers()[i].getPayload())); + values.add(createNamePortBundle(srv.getName(), srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls)); } } catch (SocketTimeoutException e) { Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress()); } } if (ips4.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4)); + values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4, tlsSrv.tls)); } else { DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress()); for(int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload())); + values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls)); } } - values.add(createNamePortBundle(srv.getName(), srv.getPort())); + values.add(createNamePortBundle(srv.getName(), srv.getPort(), tlsSrv.tls)); } bundle.putParcelableArrayList("values", values); } catch (SocketTimeoutException e) { @@ -195,28 +213,31 @@ public class DNSHelper { return bundle; } - private static Bundle createNamePortBundle(String name, int port) { + private static Bundle createNamePortBundle(String name, int port, final boolean tls) { Bundle namePort = new Bundle(); namePort.putString("name", name); + namePort.putBoolean("tls", tls); namePort.putInt("port", port); return namePort; } - private static Bundle createNamePortBundle(String name, int port, TreeMap<String, ArrayList<String>> ips) { + private static Bundle createNamePortBundle(String name, int port, Map<String, List<String>> ips, final boolean tls) { Bundle namePort = new Bundle(); namePort.putString("name", name); + namePort.putBoolean("tls", tls); namePort.putInt("port", port); if (ips!=null) { - ArrayList<String> ip = ips.get(name); + List<String> ip = ips.get(name); Collections.shuffle(ip, new Random()); namePort.putString("ip", ip.get(0)); } return namePort; } - private static Bundle createNamePortBundle(String name, int port, Data data) { + private static Bundle createNamePortBundle(String name, int port, Data data, final boolean tls) { Bundle namePort = new Bundle(); namePort.putString("name", name); + namePort.putBoolean("tls", tls); namePort.putInt("port", port); if (data instanceof A) { namePort.putString("ip", data.toString()); diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index 6d1d1074d..f2c3ea18a 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -24,6 +24,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -35,14 +36,13 @@ public class ExceptionHelper { } } - public static void checkForCrash(Context context, - final XmppConnectionService service) { + public static boolean checkForCrash(ConversationActivity activity, final XmppConnectionService service) { try { final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(context); + .getDefaultSharedPreferences(activity); boolean neverSend = preferences.getBoolean("never_send", false); if (neverSend) { - return; + return false; } List<Account> accounts = service.getAccounts(); Account account = null; @@ -53,24 +53,25 @@ public class ExceptionHelper { } } if (account == null) { - return; + return false; } final Account finalAccount = account; - FileInputStream file = context.openFileInput("stacktrace.txt"); + FileInputStream file = activity.openFileInput("stacktrace.txt"); InputStreamReader inputStreamReader = new InputStreamReader(file); BufferedReader stacktrace = new BufferedReader(inputStreamReader); final StringBuilder report = new StringBuilder(); - PackageManager pm = context.getPackageManager(); + PackageManager pm = activity.getPackageManager(); PackageInfo packageInfo = null; try { - packageInfo = pm.getPackageInfo(context.getPackageName(), 0); + packageInfo = pm.getPackageInfo(activity.getPackageName(), 0); report.append("Version: " + packageInfo.versionName + '\n'); report.append("Last Update: " - + DateUtils.formatDateTime(context, - packageInfo.lastUpdateTime, - DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_DATE) + '\n'); + + DateUtils.formatDateTime(activity, + packageInfo.lastUpdateTime, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_DATE) + '\n'); } catch (NameNotFoundException e) { + return false; } String line; while ((line = stacktrace.readLine()) != null) { @@ -78,11 +79,11 @@ public class ExceptionHelper { report.append('\n'); } file.close(); - context.deleteFile("stacktrace.txt"); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(context.getString(R.string.crash_report_title)); - builder.setMessage(context.getText(R.string.crash_report_message)); - builder.setPositiveButton(context.getText(R.string.send_now), + activity.deleteFile("stacktrace.txt"); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.crash_report_title)); + builder.setMessage(activity.getText(R.string.crash_report_message)); + builder.setPositiveButton(activity.getText(R.string.send_now), new OnClickListener() { @Override @@ -91,18 +92,19 @@ public class ExceptionHelper { Log.d(Config.LOGTAG, "using account=" + finalAccount.getJid().toBareJid() + " to send in stack trace"); - Conversation conversation = null; - try { - conversation = service.findOrCreateConversation(finalAccount, - Jid.fromString("bugs@pix-art.de"), false); - } catch (final InvalidJidException ignored) { - } - Message message = new Message(conversation, report + + Conversation conversation = null; + try { + conversation = service.findOrCreateConversation(finalAccount, + Jid.fromString("bugs@pix-art.de"), false); + } catch (final InvalidJidException ignored) { + } + Message message = new Message(conversation, report .toString(), Message.ENCRYPTION_NONE); service.sendMessage(message); } }); - builder.setNegativeButton(context.getText(R.string.send_never), + builder.setNegativeButton(activity.getText(R.string.send_never), new OnClickListener() { @Override @@ -112,8 +114,9 @@ public class ExceptionHelper { } }); builder.create().show(); + return true; } catch (final IOException ignored) { - } - + return false; + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java new file mode 100644 index 000000000..49e9a81ad --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java @@ -0,0 +1,62 @@ +package eu.siacs.conversations.utils; + +import java.lang.reflect.Method; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +public class SSLSocketHelper { + + public static void setSecurity(final SSLSocket sslSocket) throws NoSuchAlgorithmException { + final String[] supportProtocols; + final Collection<String> supportedProtocols = new LinkedList<>( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]); + + sslSocket.setEnabledProtocols(supportProtocols); + + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( + sslSocket.getSupportedCipherSuites()); + if (cipherSuites.length > 0) { + sslSocket.setEnabledCipherSuites(cipherSuites); + } + } + + public static void setSNIHost(final SSLSocketFactory factory, final SSLSocket socket, final String hostname) { + if (factory instanceof android.net.SSLCertificateSocketFactory && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + ((android.net.SSLCertificateSocketFactory) factory).setHostname(socket, hostname); + } else { + try { + socket.getClass().getMethod("setHostname", String.class).invoke(socket, hostname); + } catch (Throwable e) { + // ignore any error, we just can't set the hostname... + } + } + } + + public static void setAlpnProtocol(final SSLSocketFactory factory, final SSLSocket socket, final String protocol) { + try { + if (factory instanceof android.net.SSLCertificateSocketFactory && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + // can't call directly because of @hide? + //((android.net.SSLCertificateSocketFactory)factory).setAlpnProtocols(new byte[][]{protocol.getBytes("UTF-8")}); + android.net.SSLCertificateSocketFactory.class.getMethod("setAlpnProtocols", byte[][].class).invoke(socket, new Object[]{new byte[][]{protocol.getBytes("UTF-8")}}); + } else { + final Method method = socket.getClass().getMethod("setAlpnProtocols", byte[].class); + // the concatenation of 8-bit, length prefixed protocol names, just one in our case... + // http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 + final byte[] protocolUTF8Bytes = protocol.getBytes("UTF-8"); + final byte[] lengthPrefixedProtocols = new byte[protocolUTF8Bytes.length + 1]; + lengthPrefixedProtocols[0] = (byte) protocol.length(); // cannot be over 255 anyhow + System.arraycopy(protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length); + method.invoke(socket, new Object[]{lengthPrefixedProtocols}); + } + } catch (Throwable e) { + // ignore any error, we just can't set the alpn protocol... + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index c64fa196d..a56a64aff 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -20,7 +20,6 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.math.BigInteger; import java.net.ConnectException; import java.net.IDN; @@ -35,12 +34,9 @@ import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; @@ -66,6 +62,7 @@ import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.DNSHelper; +import eu.siacs.conversations.utils.SSLSocketHelper; import eu.siacs.conversations.utils.SocksSocketFactory; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; @@ -247,6 +244,7 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG,account.getJid().toBareJid()+": connect to "+destination+" via TOR"); socket = SocksSocketFactory.createSocketOverTor(destination,account.getPort()); + startXmpp(); } else if (DNSHelper.isIp(account.getServer().toString())) { socket = new Socket(); try { @@ -254,13 +252,12 @@ public class XmppConnection implements Runnable { } catch (IOException e) { throw new UnknownHostException(); } + startXmpp(); } else { - final Bundle result = DNSHelper.getSRVRecord(account.getServer(),mXmppConnectionService); + final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService); final ArrayList<Parcelable>values = result.getParcelableArrayList("values"); - int i = 0; - boolean socketError = true; - while (socketError && values.size() > i) { - final Bundle namePort = (Bundle) values.get(i); + for(Iterator<Parcelable> iterator = values.iterator(); iterator.hasNext();) { + final Bundle namePort = (Bundle) iterator.next(); try { String srvRecordServer; try { @@ -271,48 +268,57 @@ public class XmppConnection implements Runnable { } final int srvRecordPort = namePort.getInt("port"); final String srvIpServer = namePort.getString("ip"); + // if tls is true, encryption is implied and must not be started + features.encryptionEnabled = namePort.getBoolean("tls"); final InetSocketAddress addr; if (srvIpServer != null) { addr = new InetSocketAddress(srvIpServer, srvRecordPort); Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": using values from dns " + srvRecordServer - + "[" + srvIpServer + "]:" + srvRecordPort); + + "[" + srvIpServer + "]:" + srvRecordPort + " tls: " + features.encryptionEnabled); } else { addr = new InetSocketAddress(srvRecordServer, srvRecordPort); Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort); + + srvRecordServer + ":" + srvRecordPort + " tls: " + features.encryptionEnabled); } - socket = new Socket(); - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - socketError = false; + + if (!features.encryptionEnabled) { + socket = new Socket(); + socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); + } else { + final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); + socket = tlsFactoryVerifier.factory.createSocket(); + + if (socket == null) { + throw new IOException("could not initialize ssl socket"); + } + + SSLSocketHelper.setSecurity((SSLSocket) socket); + SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart()); + SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client"); + + socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); + + if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); + throw new SecurityException(); + } + } + + if (startXmpp()) + break; // successfully connected to server that speaks xmpp + } catch(final SecurityException e) { + throw e; } catch (final Throwable e) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() +"("+e.getClass().getName()+")"); - i++; + if (!iterator.hasNext()) { + throw new UnknownHostException(); + } } } - if (socketError) { - throw new UnknownHostException(); - } - } - final OutputStream out = socket.getOutputStream(); - tagWriter.setOutputStream(out); - final InputStream in = socket.getInputStream(); - tagReader.setInputStream(in); - tagWriter.beginDocument(); - sendStartStream(); - Tag nextTag; - while ((nextTag = tagReader.readTag()) != null) { - if (nextTag.isStart("stream")) { - processStream(); - break; - } else { - throw new IOException("unknown tag on connect"); - } - } - if (socket.isConnected()) { - socket.close(); } + processStream(); } catch (final IncompatibleServerException e) { this.changeStatus(Account.State.INCOMPATIBLE_SERVER); } catch (final SecurityException e) { @@ -344,6 +350,66 @@ public class XmppConnection implements Runnable { } } + /** + * Starts xmpp protocol, call after connecting to socket + * @return true if server returns with valid xmpp, false otherwise + * @throws IOException Unknown tag on connect + * @throws XmlPullParserException Bad Xml + * @throws NoSuchAlgorithmException Other error + */ + private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException { + tagWriter.setOutputStream(socket.getOutputStream()); + tagReader.setInputStream(socket.getInputStream()); + tagWriter.beginDocument(); + sendStartStream(); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("stream")) { + return true; + } else { + throw new IOException("unknown tag on connect"); + } + } + if (socket.isConnected()) { + socket.close(); + } + return false; + } + + private static class TlsFactoryVerifier { + private final SSLSocketFactory factory; + private final HostnameVerifier verifier; + + public TlsFactoryVerifier(final SSLSocketFactory factory, final HostnameVerifier verifier) throws IOException { + this.factory = factory; + this.verifier = verifier; + if (factory == null || verifier == null) { + throw new IOException("could not setup ssl"); + } + } + } + + private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException { + final SSLContext sc = SSLContext.getInstance("TLS"); + MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); + KeyManager[] keyManager; + if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) { + keyManager = new KeyManager[]{mKeyManager}; + } else { + keyManager = null; + } + sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG()); + final SSLSocketFactory factory = sc.getSocketFactory(); + final HostnameVerifier verifier; + if (mInteractive) { + verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier()); + } else { + verifier = trustManager.wrapHostnameVerifierNonInteractive(new XmppDomainVerifier()); + } + + return new TlsFactoryVerifier(factory, verifier); + } + @Override public void run() { try { @@ -605,53 +671,27 @@ public class XmppConnection implements Runnable { tagWriter.writeTag(startTLS); } + + private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException { tagReader.readTag(); try { - final SSLContext sc = SSLContext.getInstance("TLS"); - MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); - KeyManager[] keyManager; - if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) { - keyManager = new KeyManager[]{ mKeyManager }; - } else { - keyManager = null; - } - sc.init(keyManager,new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()},mXmppConnectionService.getRNG()); - final SSLSocketFactory factory = sc.getSocketFactory(); - final HostnameVerifier verifier; - if (mInteractive) { - verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier()); - } else { - verifier = trustManager.wrapHostnameVerifierNonInteractive(new XmppDomainVerifier()); - } + final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); final InetAddress address = socket == null ? null : socket.getInetAddress(); - if (factory == null || address == null || verifier == null) { + if (address == null) { throw new IOException("could not setup ssl"); } - final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); + final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true); if (sslSocket == null) { throw new IOException("could not initialize ssl socket"); } - final String[] supportProtocols; - final Collection<String> supportedProtocols = new LinkedList<>( - Arrays.asList(sslSocket.getSupportedProtocols())); - supportedProtocols.remove("SSLv3"); - supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]); - - sslSocket.setEnabledProtocols(supportProtocols); - - final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( - sslSocket.getSupportedCipherSuites()); - //Log.d(Config.LOGTAG, "Using ciphers: " + Arrays.toString(cipherSuites)); - if (cipherSuites.length > 0) { - sslSocket.setEnabledCipherSuites(cipherSuites); - } + SSLSocketHelper.setSecurity(sslSocket); - if (!verifier.verify(account.getServer().getDomainpart(),sslSocket.getSession())) { + if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), sslSocket.getSession())) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); throw new SecurityException(); } |