~singpolyma/cheogram-android

ceb83e49fd06c6fbe663e1d978fe673fbf8eb2a5 — Stephen Paul Weber a month ago 5288fd7
Use radio buttons when there is no existing value
A src/cheogram/java/com/cheogram/android/GridView.java => src/cheogram/java/com/cheogram/android/GridView.java +38 -0
@@ 0,0 1,38 @@
package com.cheogram.android;

import android.content.Context;
import android.util.AttributeSet;

// https://blog.jayway.com/2012/10/04/how-to-make-the-height-of-a-gridview-wrap-its-content/
public class GridView extends android.widget.GridView {
    public GridView(Context context) {
        super(context);
    }

    public GridView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public GridView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightSpec;

        if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
            // The great Android "hackatlon", the love, the magic.
            // The two leftmost bits in the height measure spec have
            // a special meaning, hence we can't use them to describe height.
            heightSpec = MeasureSpec.makeMeasureSpec(
                    Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        }
        else {
            // Any other height should be respected as is.
            heightSpec = heightMeasureSpec;
        }

        super.onMeasure(widthMeasureSpec, heightSpec);
    }
}

A src/cheogram/java/eu/siacs/conversations/xmpp/Option.java => src/cheogram/java/eu/siacs/conversations/xmpp/Option.java +41 -0
@@ 0,0 1,41 @@
package eu.siacs.conversations.xmpp;

import java.util.ArrayList;
import java.util.List;
import eu.siacs.conversations.xml.Element;

public class Option {
    protected final String value;
    protected final String label;

    public static List<Option> forField(Element field) {
        List<Option> options = new ArrayList<>();
        for (Element el : field.getChildren()) {
            if (!el.getNamespace().equals("jabber:x:data")) continue;
            if (!el.getName().equals("option")) continue;
            options.add(new Option(el));
        }
        return options;
    }

    public Option(final Element option) {
        this(option.findChildContent("value", "jabber:x:data"), option.getAttribute("label"));
    }

    public Option(final String value, final String label) {
        this.value = value;
        this.label = label == null ? value : label;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Option)) return false;

        if (value == ((Option) o).value) return true;
        if (value == null || ((Option) o).value == null) return false;
        return value.equals(((Option) o).value);
    }

    public String toString() { return label; }

    public String getValue() { return value; }
}

A src/cheogram/res/layout/command_radio_edit_field.xml => src/cheogram/res/layout/command_radio_edit_field.xml +41 -0
@@ 0,0 1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="16dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/label"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="13dp"
            android:paddingRight="8dp"
            android:paddingBottom="8dp"
            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
            android:textColor="?attr/edit_text_color" />

        <com.cheogram.android.GridView
            android:id="@+id/radios"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginRight="8dp"
            android:paddingLeft="8dp"
            android:paddingBottom="8dp"
            android:horizontalSpacing="0dp"
            android:verticalSpacing="0dp"
            android:numColumns="auto_fit" />

        <TextView
            android:id="@+id/desc"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="16dp"
            android:paddingRight="8dp"
            android:textAppearance="@style/TextAppearance.Conversations.Status"
            android:textColor="?attr/edit_text_color" />

    </LinearLayout>
</layout>

A src/cheogram/res/layout/radio_grid_item.xml => src/cheogram/res/layout/radio_grid_item.xml +9 -0
@@ 0,0 1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <RadioButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</layout>

M src/main/java/eu/siacs/conversations/entities/Conversation.java => src/main/java/eu/siacs/conversations/entities/Conversation.java +115 -34
@@ 6,6 6,8 @@ import android.database.DataSetObserver;
import android.net.Uri;
import android.text.Editable;
import android.text.InputType;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;


@@ 19,6 21,7 @@ import android.widget.TextView;
import android.widget.Spinner;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;


@@ 53,6 56,7 @@ import eu.siacs.conversations.databinding.CommandPageBinding;
import eu.siacs.conversations.databinding.CommandNoteBinding;
import eu.siacs.conversations.databinding.CommandResultFieldBinding;
import eu.siacs.conversations.databinding.CommandCheckboxFieldBinding;
import eu.siacs.conversations.databinding.CommandRadioEditFieldBinding;
import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
import eu.siacs.conversations.databinding.CommandTextFieldBinding;
import eu.siacs.conversations.databinding.CommandWebviewBinding;


@@ 66,6 70,7 @@ import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.Option;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.mam.MamReference;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;


@@ 1421,6 1426,75 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                }
            }

            class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener {
                public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
                    super(binding);
                    options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.radio_grid_item) {
                        @Override
                        public View getView(int position, View convertView, ViewGroup parent) {
                            CompoundButton v = (CompoundButton) super.getView(position, convertView, parent);
                            v.setId(position);
                            v.setChecked(getItem(position).getValue().equals(mValue.getContent()));
                            v.setOnCheckedChangeListener(RadioEditFieldViewHolder.this);
                            return v;
                        }
                    };
                }
                protected Element mValue = null;
                protected ArrayAdapter<Option> options;

                @Override
                public void bind(Element field) {
                    String label = field.getAttribute("label");
                    if (label == null) label = field.getAttribute("var");
                    if (label == null) {
                        binding.label.setVisibility(View.GONE);
                    } else {
                        binding.label.setVisibility(View.VISIBLE);
                        binding.label.setText(label);
                    }

                    String desc = field.findChildContent("desc", "jabber:x:data");
                    if (desc == null) {
                        binding.desc.setVisibility(View.GONE);
                    } else {
                        binding.desc.setVisibility(View.VISIBLE);
                        binding.desc.setText(desc);
                    }

                    mValue = field.findChild("value", "jabber:x:data");
                    if (mValue == null) {
                        mValue = field.addChild("value", "jabber:x:data");
                    }

                    options.clear();
                    List<Option> theOptions = Option.forField(field);
                    options.addAll(theOptions);

                    float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
                    TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
                    float maxColumnWidth = theOptions.stream().map((x) ->
                        StaticLayout.getDesiredWidth(x.toString(), paint)
                    ).max(Float::compare).orElse(new Float(0.0));
                    if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
                        binding.radios.setNumColumns(theOptions.size());
                    } else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
                        binding.radios.setNumColumns(theOptions.size() / 2);
                    } else {
                        binding.radios.setNumColumns(1);
                    }
                    binding.radios.setAdapter(options);
                }

                @Override
                public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
                    if (mValue == null) return;

                    if (isChecked) mValue.setContent(options.getItem(radio.getId()).getValue());
                    options.notifyDataSetChanged();
                }
            }

            class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
                public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
                    super(binding);


@@ 1455,11 1529,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl

                    ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
                    options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
                    for (Element el : field.getChildren()) {
                        if (!el.getNamespace().equals("jabber:x:data")) continue;
                        if (!el.getName().equals("option")) continue;
                        options.add(new Option(el));
                    }
                    options.addAll(Option.forField(field));

                    binding.spinner.setAdapter(options);
                    binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
                }


@@ 1469,35 1540,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                    Option o = (Option) parent.getItemAtPosition(pos);
                    if (mValue == null) return;

                    mValue.setContent(o == null ? "" : o.value);
                    mValue.setContent(o == null ? "" : o.getValue());
                }

                @Override
                public void onNothingSelected(AdapterView<?> parent) {
                    mValue.setContent("");
                }

                class Option {
                    protected final String value;
                    protected final String label;

                    public Option(final Element option) {
                        this(option.findChildContent("value", "jabber:x:data"), option.getAttribute("label"));
                    }

                    public Option(final String value, final String label) {
                        this.value = value;
                        this.label = label == null ? value : label;
                    }

                    public boolean equals(Object o) {
                        if (!(o instanceof Option)) return false;

                        return value.equals(((Option) o).value);
                    }

                    public String toString() { return label; }
                }
            }

            class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {


@@ 1597,11 1646,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
            final int TYPE_TEXT_FIELD = 5;
            final int TYPE_CHECKBOX_FIELD = 6;
            final int TYPE_SPINNER_FIELD = 7;
            final int TYPE_RADIO_EDIT_FIELD = 8;

            protected String mTitle;
            protected CommandPageBinding mBinding = null;
            protected IqPacket response = null;
            protected Element responseElement = null;
            protected SparseArray<Integer> viewTypes = new SparseArray<>();
            protected XmppConnectionService xmppConnectionService;
            protected ArrayAdapter<String> actionsAdapter;



@@ 1641,6 1692,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
            public void updateWithResponse(IqPacket iq) {
                this.responseElement = null;
                this.response = iq;
                this.viewTypes.clear();
                this.actionsAdapter.clear();

                Element command = iq.findChild("command", "http://jabber.org/protocol/commands");


@@ 1743,13 1795,23 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl

            @Override
            public int getItemViewType(int position) {
                if (viewTypes.get(position) != null) return viewTypes.get(position);
                if (response == null) return -1;

                if (response.getType() == IqPacket.TYPE.RESULT) {
                    Element item = getItem(position);
                    if (item.getName().equals("note")) return TYPE_NOTE;
                    if (item.getNamespace().equals("jabber:x:oob")) return TYPE_WEB;
                    if (item.getName().equals("instructions") && item.getNamespace().equals("jabber:x:data")) return TYPE_NOTE;
                    if (item.getName().equals("note")) {
                        viewTypes.put(position, TYPE_NOTE);
                        return TYPE_NOTE;
                    }
                    if (item.getNamespace().equals("jabber:x:oob")) {
                        viewTypes.put(position, TYPE_WEB);
                        return TYPE_WEB;
                    }
                    if (item.getName().equals("instructions") && item.getNamespace().equals("jabber:x:data")) {
                        viewTypes.put(position, TYPE_NOTE);
                        return TYPE_NOTE;
                    }
                    if (item.getName().equals("field") && item.getNamespace().equals("jabber:x:data")) {
                        String formType = responseElement.getAttribute("type");
                        if (formType == null) return -1;


@@ 1757,11 1819,26 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                        String fieldType = item.getAttribute("type");
                        if (fieldType == null) fieldType = "text-single";

                        if (formType.equals("result") || fieldType.equals("fixed")) return TYPE_RESULT_FIELD;
                        if (formType.equals("result") || fieldType.equals("fixed")) {
                            viewTypes.put(position, TYPE_RESULT_FIELD);
                            return TYPE_RESULT_FIELD;
                        }
                        if (formType.equals("form")) {
                            if (fieldType.equals("boolean")) return TYPE_CHECKBOX_FIELD;
                            if (fieldType.equals("list-single")) return TYPE_SPINNER_FIELD;
                            viewTypes.put(position, TYPE_CHECKBOX_FIELD);
                            if (fieldType.equals("boolean")) {
                                return TYPE_CHECKBOX_FIELD;
                            }
                            if (fieldType.equals("list-single")) {
                                if (item.findChild("value", "jabber:x:data") == null) {
                                    viewTypes.put(position, TYPE_RADIO_EDIT_FIELD);
                                    return TYPE_RADIO_EDIT_FIELD;
                                }

                                viewTypes.put(position, TYPE_SPINNER_FIELD);
                                return TYPE_SPINNER_FIELD;
                            }

                            viewTypes.put(position, TYPE_TEXT_FIELD);
                            return TYPE_TEXT_FIELD;
                        }
                    }


@@ 1794,6 1871,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                        CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
                        return new CheckboxFieldViewHolder(binding);
                    }
                    case TYPE_RADIO_EDIT_FIELD: {
                        CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
                        return new RadioEditFieldViewHolder(binding);
                    }
                    case TYPE_SPINNER_FIELD: {
                        CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
                        return new SpinnerFieldViewHolder(binding);