diff options
20 files changed, 459 insertions, 75 deletions
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index c8253dd49..43a900109 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -79,6 +79,19 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } } + public boolean fetchMapHasErrors(Contact contact) { + Jid jid = contact.getJid().toBareJid(); + if (deviceIds.get(jid) != null) { + for (Integer foreignId : this.deviceIds.get(jid)) { + AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); + if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { + return true; + } + } + } + return false; + } + private static class AxolotlAddressMap<T> { protected Map<String, Map<Integer, T>> map; protected final Object MAP_LOCK = new Object(); diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index c5831e7ec..f3d891e86 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -743,13 +743,20 @@ public class Message extends AbstractEntity { } public boolean isValidInSession() { - int pastEncryption = this.getPreviousEncryption(); - int futureEncryption = this.getNextEncryption(); + int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); + int futureEncryption = getCleanedEncryption(this.getNextEncryption()); boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE || futureEncryption == ENCRYPTION_NONE || pastEncryption != futureEncryption; - return inUnencryptedSession || this.getEncryption() == pastEncryption; + return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; + } + + private static int getCleanedEncryption(int encryption) { + if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { + return ENCRYPTION_PGP; + } + return encryption; } } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 05932acce..4bdf080cf 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -86,7 +86,7 @@ public class FileBackend { public static String getConversationsImageDirectory() { return Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES).getAbsolutePath() + Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/Conversations/"; } @@ -155,12 +155,7 @@ public class FileBackend { return FileUtils.getPath(mXmppConnectionService,uri); } - public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { - Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); - String mime = mXmppConnectionService.getContentResolver().getType(uri); - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); - message.setRelativeFilePath(message.getUuid() + "." + extension); - DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); + public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { file.getParentFile().mkdirs(); OutputStream os = null; InputStream is = null; @@ -183,28 +178,18 @@ public class FileBackend { close(os); close(is); } - Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message)); - return file; + Log.d(Config.LOGTAG, "output file name " + file.getAbsolutePath()); } - public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) - throws FileCopyException { - return this.copyImageToPrivateStorage(message, image, 0); + public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { + Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); + String mime = mXmppConnectionService.getContentResolver().getType(uri); + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); + message.setRelativeFilePath(message.getUuid() + "." + extension); + copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); } - private DownloadableFile copyImageToPrivateStorage(Message message,Uri image, int sampleSize) throws FileCopyException { - switch(Config.IMAGE_FORMAT) { - case JPEG: - message.setRelativeFilePath(message.getUuid()+".jpg"); - break; - case PNG: - message.setRelativeFilePath(message.getUuid()+".png"); - break; - case WEBP: - message.setRelativeFilePath(message.getUuid()+".webp"); - break; - } - DownloadableFile file = getFile(message); + private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException { file.getParentFile().mkdirs(); InputStream is = null; OutputStream os = null; @@ -225,7 +210,6 @@ public class FileBackend { int rotation = getRotation(image); scaledBitmap = rotate(scaledBitmap, rotation); boolean targetSizeReached = false; - long size = 0; int quality = Config.IMAGE_QUALITY; while(!targetSizeReached) { os = new FileOutputStream(file); @@ -234,14 +218,11 @@ public class FileBackend { throw new FileCopyException(R.string.error_compressing_image); } os.flush(); - size = file.getSize(); - targetSizeReached = size <= Config.IMAGE_MAX_SIZE || quality <= 50; + targetSizeReached = file.length() <= Config.IMAGE_MAX_SIZE || quality <= 50; quality -= 5; } - int width = scaledBitmap.getWidth(); - int height = scaledBitmap.getHeight(); - message.setBody(Long.toString(size) + '|' + width + '|' + height); - return file; + scaledBitmap.recycle(); + return; } catch (FileNotFoundException e) { throw new FileCopyException(R.string.error_file_not_found); } catch (IOException e) { @@ -252,7 +233,7 @@ public class FileBackend { } catch (OutOfMemoryError e) { ++sampleSize; if (sampleSize <= 3) { - return copyImageToPrivateStorage(message, image, sampleSize); + copyImageToPrivateStorage(file, image, sampleSize); } else { throw new FileCopyException(R.string.error_out_of_memory); } @@ -264,6 +245,26 @@ public class FileBackend { } } + public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException { + copyImageToPrivateStorage(file, image, 0); + } + + public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException { + switch(Config.IMAGE_FORMAT) { + case JPEG: + message.setRelativeFilePath(message.getUuid()+".jpg"); + break; + case PNG: + message.setRelativeFilePath(message.getUuid()+".png"); + break; + case WEBP: + message.setRelativeFilePath(message.getUuid()+".webp"); + break; + } + copyImageToPrivateStorage(getFile(message), image); + updateFileParams(message); + } + private int getRotation(File file) { return getRotation(Uri.parse("file://"+file.getAbsolutePath())); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a295b2cee..d4fda1b86 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1969,7 +1969,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } - Element form = query.findChild("x","jabber:x:data"); + Element form = query.findChild("x", "jabber:x:data"); if (form != null) { conversation.getMucOptions().updateFormData(Data.parse(form)); } @@ -2378,7 +2378,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa updateConversationUi(); updateRosterUi(); } else { - Conversation conversation = find(account,avatar.owner.toBareJid()); + Conversation conversation = find(account, avatar.owner.toBareJid()); if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { MucOptions.User user = conversation.getMucOptions().findUser(avatar.owner.getResourcepart()); if (user != null) { @@ -2586,6 +2586,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return Config.FORCE_ORBOT || getPreferences().getBoolean("use_tor", false); } + public boolean showExtendedConnectionOptions() { + return getPreferences().getBoolean("show_connection_options", false); + } + public int unreadCount() { int count = 0; for (Conversation conversation : getConversations()) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 0ea393296..a126b344d 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -5,6 +5,7 @@ import android.app.AlertDialog.Builder; import android.app.PendingIntent; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; @@ -91,7 +92,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private Jid jidToEdit; private boolean mInitMode = false; - private boolean mUseTor = false; + private boolean mShowOptions = false; private Account mAccount; private String messageFingerprint; @@ -133,7 +134,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } String hostname = null; int numericPort = 5222; - if (mUseTor) { + if (mShowOptions) { hostname = mHostname.getText().toString(); final String port = mPort.getText().toString(); if (hostname.contains(" ")) { @@ -511,8 +512,11 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } } } - this.mUseTor = Config.FORCE_ORBOT || getPreferences().getBoolean("use_tor", false); - this.mNamePort.setVisibility(mUseTor ? View.VISIBLE : View.GONE); + SharedPreferences preferences = getPreferences(); + boolean useTor = Config.FORCE_ORBOT || preferences.getBoolean("use_tor", false); + this.mShowOptions = useTor || preferences.getBoolean("show_connection_options", false); + mHostname.setHint(useTor ? R.string.hostname_or_onion : R.string.hostname_example); + this.mNamePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); } @Override @@ -598,7 +602,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mHostname.getEditableText().append(this.mAccount.getHostname()); this.mPort.setText(""); this.mPort.getEditableText().append(String.valueOf(this.mAccount.getPort())); - this.mNamePort.setVisibility(mUseTor ? View.VISIBLE : View.GONE); + this.mNamePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); } if (!mInitMode) { @@ -740,12 +744,24 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } } else { if (this.mAccount.errorStatus()) { - this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); + final EditText errorTextField; + if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED) { + errorTextField = this.mPassword; + } else if (mShowOptions + && this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND + && this.mHostname.getText().length() > 0) { + errorTextField = this.mHostname; + } else { + errorTextField = this.mAccountJid; + } + errorTextField.setError(getString(this.mAccount.getStatus().getReadableId())); if (init || !accountInfoEdited()) { - this.mAccountJid.requestFocus(); + errorTextField.requestFocus(); } } else { this.mAccountJid.setError(null); + this.mPassword.setError(null); + this.mHostname.setError(null); } this.mStats.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index 29da0ce6c..eec30798e 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -162,7 +162,8 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate } else { if (!hasForeignKeys && hasNoOtherTrustedKeys()) { keyErrorMessageCard.setVisibility(View.VISIBLE); - if (lastFetchReport == AxolotlService.FetchStatus.ERROR) { + if (lastFetchReport == AxolotlService.FetchStatus.ERROR + || contact.getAccount().getAxolotlService().fetchMapHasErrors(contact)) { keyErrorMessage.setText(R.string.error_no_keys_to_trust_server_error); } else { keyErrorMessage.setText(R.string.error_no_keys_to_trust); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 4be4931f7..47414f900 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -78,9 +78,10 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { } final Jid jid = item.getJid(); if (jid != null) { + tvJid.setVisibility(View.VISIBLE); tvJid.setText(jid.toString()); } else { - tvJid.setText(""); + tvJid.setVisibility(View.GONE); } tvName.setText(item.getDisplayName()); loadAvatar(item,picture); diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java new file mode 100644 index 000000000..6cb357a90 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java @@ -0,0 +1,80 @@ +package eu.siacs.conversations.ui.forms; + +import android.content.Context; +import android.widget.CheckBox; +import android.widget.CompoundButton; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.xmpp.forms.Field; + +public class FormBooleanFieldWrapper extends FormFieldWrapper { + + protected CheckBox checkBox; + + protected FormBooleanFieldWrapper(Context context, Field field) { + super(context, field); + checkBox = (CheckBox) view.findViewById(R.id.field); + checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + checkBox.setError(null); + invokeOnFormFieldValuesEdited(); + } + }); + } + + @Override + protected void setLabel(String label, boolean required) { + CheckBox checkBox = (CheckBox) view.findViewById(R.id.field); + checkBox.setText(createSpannableLabelString(label, required)); + } + + @Override + public List<String> getValues() { + List<String> values = new ArrayList<>(); + values.add(Boolean.toString(checkBox.isChecked())); + return values; + } + + @Override + protected void setValues(List<String> values) { + if (values.size() == 0) { + checkBox.setChecked(false); + } else { + checkBox.setChecked(Boolean.parseBoolean(values.get(0))); + } + } + + @Override + public boolean validates() { + if (checkBox.isChecked() || !field.isRequired()) { + return true; + } else { + checkBox.setError(context.getString(R.string.this_field_is_required)); + checkBox.requestFocus(); + return false; + } + } + + @Override + public boolean edited() { + if (field.getValues().size() == 0) { + return checkBox.isChecked(); + } else { + return super.edited(); + } + } + + @Override + protected int getLayoutResource() { + return R.layout.form_boolean; + } + + @Override + void setReadOnly(boolean readOnly) { + checkBox.setEnabled(!readOnly); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java index 9e54678a3..ee3064726 100644 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java +++ b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java @@ -15,10 +15,16 @@ public class FormFieldFactory { static { typeTable.put("text-single", FormTextFieldWrapper.class); typeTable.put("text-multi", FormTextFieldWrapper.class); + typeTable.put("text-private", FormTextFieldWrapper.class); + typeTable.put("jid-single", FormJidSingleFieldWrapper.class); + typeTable.put("boolean", FormBooleanFieldWrapper.class); } - public static FormFieldWrapper createFromField(Context context, Field field) { + protected static FormFieldWrapper createFromField(Context context, Field field) { Class clazz = typeTable.get(field.getType()); + if (clazz == null) { + clazz = FormTextFieldWrapper.class; + } return FormFieldWrapper.createFromField(clazz, context, field); } } diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java index 3af375363..3a21ade3b 100644 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java +++ b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java @@ -1,11 +1,17 @@ package eu.siacs.conversations.ui.forms; import android.content.Context; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import java.util.List; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; import eu.siacs.conversations.xmpp.forms.Field; public abstract class FormFieldWrapper { @@ -13,20 +19,25 @@ public abstract class FormFieldWrapper { protected final Context context; protected final Field field; protected final View view; + protected OnFormFieldValuesEdited onFormFieldValuesEditedListener; protected FormFieldWrapper(Context context, Field field) { this.context = context; this.field = field; LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.view = inflater.inflate(getLayoutResource(), null); - setLabel(field.getLabel(), field.isRequired()); + String label = field.getLabel(); + if (label == null) { + label = field.getFieldName(); + } + setLabel(label, field.isRequired()); } - public void submit() { + public final void submit() { this.field.setValues(getValues()); } - public View getView() { + public final View getView() { return view; } @@ -34,14 +45,51 @@ public abstract class FormFieldWrapper { abstract List<String> getValues(); + protected abstract void setValues(List<String> values); + + abstract boolean validates(); + abstract protected int getLayoutResource(); + abstract void setReadOnly(boolean readOnly); + + protected SpannableString createSpannableLabelString(String label, boolean required) { + SpannableString spannableString = new SpannableString(label + (required ? " *" : "")); + if (required) { + int start = label.length(); + int end = label.length() + 2; + spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0); + spannableString.setSpan(new ForegroundColorSpan(context.getResources().getColor(R.color.accent)), start, end, 0); + } + return spannableString; + } + + protected void invokeOnFormFieldValuesEdited() { + if (this.onFormFieldValuesEditedListener != null) { + this.onFormFieldValuesEditedListener.onFormFieldValuesEdited(); + } + } + + public boolean edited() { + return !field.getValues().equals(getValues()); + } + + public void setOnFormFieldValuesEditedListener(OnFormFieldValuesEdited listener) { + this.onFormFieldValuesEditedListener = listener; + } + protected static <F extends FormFieldWrapper> FormFieldWrapper createFromField(Class<F> c, Context context, Field field) { try { - return c.getDeclaredConstructor(Context.class, Field.class).newInstance(context,field); + F fieldWrapper = c.getDeclaredConstructor(Context.class, Field.class).newInstance(context,field); + fieldWrapper.setValues(field.getValues()); + return fieldWrapper; } catch (Exception e) { e.printStackTrace(); return null; } } + + public interface OnFormFieldValuesEdited { + void onFormFieldValuesEdited(); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java new file mode 100644 index 000000000..553e8f21f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java @@ -0,0 +1,44 @@ +package eu.siacs.conversations.ui.forms; + +import android.content.Context; +import android.text.InputType; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.xmpp.forms.Field; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class FormJidSingleFieldWrapper extends FormTextFieldWrapper { + + protected FormJidSingleFieldWrapper(Context context, Field field) { + super(context, field); + editText.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + editText.setHint(R.string.account_settings_example_jabber_id); + } + + @Override + public boolean validates() { + String value = getValue(); + if (!value.isEmpty()) { + try { + Jid.fromString(value); + } catch (InvalidJidException e) { + editText.setError(context.getString(R.string.invalid_jid)); + editText.requestFocus(); + return false; + } + } + return super.validates(); + } + + @Override + protected void setValues(List<String> values) { + StringBuilder builder = new StringBuilder(""); + for(String value : values) { + builder.append(value); + } + editText.setText(builder.toString()); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java index 8f70dd37e..b7dac9510 100644 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java +++ b/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java @@ -1,9 +1,9 @@ package eu.siacs.conversations.ui.forms; import android.content.Context; -import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; import android.widget.EditText; import android.widget.TextView; @@ -20,33 +20,78 @@ public class FormTextFieldWrapper extends FormFieldWrapper { protected FormTextFieldWrapper(Context context, Field field) { super(context, field); editText = (EditText) view.findViewById(R.id.field); - editText.setSingleLine("text-single".equals(field.getType())); + editText.setSingleLine(!"text-multi".equals(field.getType())); + if ("text-private".equals(field.getType())) { + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + editText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + editText.setError(null); + invokeOnFormFieldValuesEdited(); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); } @Override protected void setLabel(String label, boolean required) { TextView textView = (TextView) view.findViewById(R.id.label); - SpannableString spannableString = new SpannableString(label + (required ? " *" : "")); - if (required) { - int start = label.length(); - int end = label.length() + 2; - spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0); - spannableString.setSpan(new ForegroundColorSpan(context.getResources().getColor(R.color.accent)), start, end, 0); - } - textView.setText(spannableString); + textView.setText(createSpannableLabelString(label, required)); + } + + protected String getValue() { + return editText.getText().toString(); } @Override - List<String> getValues() { + public List<String> getValues() { List<String> values = new ArrayList<>(); - for (String line : editText.getText().toString().split("\\n")) { - values.add(line); + for (String line : getValue().split("\\n")) { + if (line.length() > 0) { + values.add(line); + } } return values; } @Override + protected void setValues(List<String> values) { + StringBuilder builder = new StringBuilder(""); + for(int i = 0; i < values.size(); ++i) { + builder.append(values.get(i)); + if (i < values.size() - 1 && "text-multi".equals(field.getType())) { + builder.append("\n"); + } + } + editText.setText(builder.toString()); + } + + @Override + public boolean validates() { + if (getValue().trim().length() > 0 || !field.isRequired()) { + return true; + } else { + editText.setError(context.getString(R.string.this_field_is_required)); + editText.requestFocus(); + return false; + } + } + + @Override protected int getLayoutResource() { return R.layout.form_text; } + + @Override + void setReadOnly(boolean readOnly) { + editText.setEnabled(!readOnly); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java new file mode 100644 index 000000000..eafe95cc8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java @@ -0,0 +1,72 @@ +package eu.siacs.conversations.ui.forms; + +import android.content.Context; +import android.widget.LinearLayout; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.forms.Field; + +public class FormWrapper { + + private final LinearLayout layout; + + private final Data form; + + private final List<FormFieldWrapper> fieldWrappers = new ArrayList<>(); + + private FormWrapper(Context context, LinearLayout linearLayout, Data form) { + this.form = form; + this.layout = linearLayout; + this.layout.removeAllViews(); + for(Field field : form.getFields()) { + FormFieldWrapper fieldWrapper = FormFieldFactory.createFromField(context,field); + if (fieldWrapper != null) { + layout.addView(fieldWrapper.getView()); + fieldWrappers.add(fieldWrapper); + } + } + } + + public Data submit() { + for(FormFieldWrapper fieldWrapper : fieldWrappers) { + fieldWrapper.submit(); + } + this.form.submit(); + return this.form; + } + + public boolean validates() { + boolean validates = true; + for(FormFieldWrapper fieldWrapper : fieldWrappers) { + validates &= fieldWrapper.validates(); + } + return validates; + } + + public void setOnFormFieldValuesEditedListener(FormFieldWrapper.OnFormFieldValuesEdited listener) { + for(FormFieldWrapper fieldWrapper : fieldWrappers) { + fieldWrapper.setOnFormFieldValuesEditedListener(listener); + } + } + + public void setReadOnly(boolean b) { + for(FormFieldWrapper fieldWrapper : fieldWrappers) { + fieldWrapper.setReadOnly(b); + } + } + + public boolean edited() { + boolean edited = false; + for(FormFieldWrapper fieldWrapper : fieldWrappers) { + edited |= fieldWrapper.edited(); + } + return edited; + } + + public static FormWrapper createInLayout(Context context, LinearLayout layout, Data form) { + return new FormWrapper(context, layout, form); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d957a60d8..cc2579a9a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -243,6 +243,7 @@ public class XmppConnection implements Runnable { tagWriter = new TagWriter(); this.changeStatus(Account.State.CONNECTING); final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion(); + final boolean extended = mXmppConnectionService.showExtendedConnectionOptions(); if (useTor) { String destination; if (account.getHostname() == null || account.getHostname().isEmpty()) { @@ -250,8 +251,16 @@ public class XmppConnection implements Runnable { } else { destination = account.getHostname(); } - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": connect to "+destination+" via TOR"); - socket = SocksSocketFactory.createSocketOverTor(destination,account.getPort()); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via TOR"); + socket = SocksSocketFactory.createSocketOverTor(destination, account.getPort()); + startXmpp(); + } else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) { + socket = new Socket(); + try { + socket.connect(new InetSocketAddress(account.getHostname(), account.getPort()), Config.SOCKET_TIMEOUT * 1000); + } catch (IOException e) { + throw new UnknownHostException(); + } startXmpp(); } else if (DNSHelper.isIp(account.getServer().toString())) { socket = new Socket(); @@ -540,7 +549,7 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("failed")) { tagReader.readElement(nextTag); Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed"); - streamId = null; + resetStreamId(); if (account.getStatus() != Account.State.ONLINE) { sendBindRequest(); } @@ -879,6 +888,7 @@ public class XmppConnection implements Runnable { public void resetEverything() { resetStreamId(); clearIqCallbacks(); + mStanzaQueue.clear(); synchronized (this.disco) { disco.clear(); } @@ -1280,7 +1290,6 @@ public class XmppConnection implements Runnable { } return; } else { - resetStreamId(); if (tagWriter.isActive()) { tagWriter.finish(); try { diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java index d05c9abb3..0053a399c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -53,16 +53,16 @@ public class Data extends Element { public void submit() { this.setAttribute("type","submit"); - removeNonFieldChildren(); + removeUnnecessaryChildren(); for(Field field : getFields()) { field.removeNonValueChildren(); } } - private void removeNonFieldChildren() { + private void removeUnnecessaryChildren() { for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) { Element element = iterator.next(); - if (!element.getName().equals("field")) { + if (!element.getName().equals("field") && !element.getName().equals("title")) { iterator.remove(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java index c1fc808dc..020b34b90 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java @@ -1,7 +1,9 @@ package eu.siacs.conversations.xmpp.forms; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; import eu.siacs.conversations.xml.Element; @@ -52,6 +54,19 @@ public class Field extends Element { return findChildContent("value"); } + public List<String> getValues() { + List<String> values = new ArrayList<>(); + for(Element child : getChildren()) { + if ("value".equals(child.getName())) { + String content = child.getContent(); + if (content != null) { + values.add(content); + } + } + } + return values; + } + public String getLabel() { return getAttribute("label"); } diff --git a/src/main/res/layout/form_boolean.xml b/src/main/res/layout/form_boolean.xml new file mode 100644 index 000000000..fd553acb2 --- /dev/null +++ b/src/main/res/layout/form_boolean.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="8dp" + android:orientation="vertical"> + <CheckBox + android:id="@+id/field" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody"/> +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/form_text.xml b/src/main/res/layout/form_text.xml index e36ac74e9..31b521e89 100644 --- a/src/main/res/layout/form_text.xml +++ b/src/main/res/layout/form_text.xml @@ -2,12 +2,12 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingTop="8dp" android:orientation="vertical"> <TextView android:id="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="8dp" android:textColor="@color/black87" android:textSize="?attr/TextSizeBody"/> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index e4fbeff0e..dcf25f50f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -543,6 +543,9 @@ <string name="pref_xa_on_silent_mode">Not available in silent mode</string> <string name="update_info">Pix-Art Messenger is checking for an update. If an update is available you will be asked, if you want to update your version. The update process is downloading and installing the new version automatically.</string> <string name="pref_xa_on_silent_mode_summary">Marks your resource as not available when device is in silent mode</string> + <string name="pref_show_connection_options">Extended connection options</string> + <string name="pref_show_connection_options_summary">Show hostname and port options when setting up an account</string> + <string name="hostname_example">xmpp.example.com</string> <string name="action_add_account_with_certificate">Add account with certificate</string> <string name="unable_to_parse_certificate">Unable to parse certificate</string> <string name="authenticate_with_certificate">Leave empty to authenticate w/ certificate</string> @@ -603,4 +606,5 @@ <string name="yes">Yes</string> <string name="no">No</string> <string name="no_accounts">(No activated accounts)</string> + <string name="this_field_is_required">This field is required</string> </resources> diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index aac580a98..ca6af3f2a 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -175,6 +175,11 @@ android:key="use_tor" android:summary="@string/pref_use_tor_summary" android:title="@string/pref_use_tor"/> + <CheckBoxPreference + android:defaultValue="false" + android:key="show_connection_options" + android:summary="@string/pref_show_connection_options_summary" + android:title="@string/pref_show_connection_options"/> </PreferenceCategory> <PreferenceCategory android:title="@string/pref_input_options"> <CheckBoxPreference |