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/drawable/list_choice.xml => src/cheogram/res/drawable/list_choice.xml +9 -0
@@ 0,0 1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:enterFadeDuration="@android:integer/config_shortAnimTime"
+ android:exitFadeDuration="@android:integer/config_shortAnimTime">
+
+ <item android:state_pressed="true" android:drawable="@color/grey500" />
+ <item android:state_activated="true" android:drawable="@color/grey500" />
+ <item android:drawable="@android:color/transparent" />
+</selector>
A src/cheogram/res/layout/command_button.xml => src/cheogram/res/layout/command_button.xml +8 -0
@@ 0,0 1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+<TextView
+ android:id="@+id/command"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:attr/buttonStyleSmall" />
+</layout>
A src/cheogram/res/layout/command_checkbox_field.xml => src/cheogram/res/layout/command_checkbox_field.xml +49 -0
@@ 0,0 1,49 @@
+<?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">
+
+ <RelativeLayout
+ android:id="@+id/row"
+ android:background="?selectableItemBackground"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/checkbox"
+ android:layout_toStartOf="@+id/checkbox"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="13dp"
+ android:scrollHorizontally="false"
+ android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
+
+ <TextView
+ android:id="@+id/desc"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="16dp"
+ android:textAppearance="@style/TextAppearance.Conversations.Status"
+ android:textColor="?android:textColorSecondary" />
+
+ </LinearLayout>
+
+ <CheckBox
+ android:id="@+id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:paddingRight="16dp"
+ android:layout_marginLeft="16dp" />
+
+ </RelativeLayout>
+</layout>
A src/cheogram/res/layout/command_note.xml => src/cheogram/res/layout/command_note.xml +31 -0
@@ 0,0 1,31 @@
+<?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">
+
+ <ImageView
+ android:visibility="gone"
+ android:id="@+id/error_icon"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_centerHorizontal="true"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_send_cancel_dnd" />
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1"
+ android:textColor="?attr/edit_text_color" />
+
+ </LinearLayout>
+</layout>
A src/cheogram/res/layout/command_page.xml => src/cheogram/res/layout/command_page.xml +29 -0
@@ 0,0 1,29 @@
+<?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">
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/form"
+ android:paddingTop="8dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/actions"
+ android:orientation="vertical"
+ app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />
+
+ <GridView
+ android:id="@+id/actions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:horizontalSpacing="0dp"
+ android:verticalSpacing="0dp"
+ android:numColumns="2" />
+
+ </RelativeLayout>
+</layout>
A src/cheogram/res/layout/command_progress_bar.xml => src/cheogram/res/layout/command_progress_bar.xml +13 -0
@@ 0,0 1,13 @@
+<?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">
+
+ <ProgressBar
+ android:id="@+id/progressbar"
+ android:layout_width="match_parent"
+ android:layout_height="130dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingBottom="16dp" />
+
+</layout>
A src/cheogram/res/layout/command_radio_edit_field.xml => src/cheogram/res/layout/command_radio_edit_field.xml +53 -0
@@ 0,0 1,53 @@
+<?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:horizontalSpacing="0dp"
+ android:verticalSpacing="0dp"
+ android:numColumns="auto_fit" />
+
+ <EditText
+ android:id="@+id/open"
+ android:visibility="gone"
+ style="@style/Widget.Conversations.EditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:ems="10"
+ android:imeOptions="actionNext"
+ android:inputType="textWebEditText"
+ android:minLines="1" />
+
+ <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="?android:textColorSecondary" />
+
+ </LinearLayout>
+</layout>
A src/cheogram/res/layout/command_result_cell.xml => src/cheogram/res/layout/command_result_cell.xml +12 -0
@@ 0,0 1,12 @@
+<?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">
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1"
+ android:textColor="?attr/edit_text_color" />
+
+</layout>
A src/cheogram/res/layout/command_result_field.xml => src/cheogram/res/layout/command_result_field.xml +42 -0
@@ 0,0 1,42 @@
+<?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:gravity="center"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingBottom="16dp"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1"
+ android:textColor="?attr/edit_text_color" />
+
+ <ListView
+ android:id="@+id/values"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="?attr/color_background_secondary"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp"></ListView>
+
+ <TextView
+ android:id="@+id/desc"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="@style/TextAppearance.Conversations.Status"
+ android:textColor="?android:textColorSecondary" />
+
+ </LinearLayout>
+</layout>
A src/cheogram/res/layout/command_search_list_field.xml => src/cheogram/res/layout/command_search_list_field.xml +47 -0
@@ 0,0 1,47 @@
+<?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" />
+
+ <EditText
+ android:id="@+id/search"
+ style="@style/Widget.Conversations.EditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:ems="10"
+ android:imeOptions="actionNext"
+ android:minLines="1" />
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="200dp"
+ android:choiceMode="singleChoice" />
+
+ <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="?android:textColorSecondary" />
+
+ </LinearLayout>
+</layout>
A src/cheogram/res/layout/command_spinner_field.xml => src/cheogram/res/layout/command_spinner_field.xml +38 -0
@@ 0,0 1,38 @@
+<?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" />
+
+ <Spinner
+ android:id="@+id/spinner"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8dp"
+ android:paddingLeft="8dp"
+ android:paddingBottom="8dp" />
+
+ <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="?android:textColorSecondary" />
+
+ </LinearLayout>
+</layout>
A src/cheogram/res/layout/command_text_field.xml => src/cheogram/res/layout/command_text_field.xml +29 -0
@@ 0,0 1,29 @@
+<?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">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/textinput_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingBottom="16dp"
+ app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"
+ app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
+ app:helperTextTextAppearance="@style/TextAppearance.Conversations.Status"
+ app:helperTextTextColor="?android:textColorSecondary">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/textinput"
+ style="@style/Widget.Conversations.EditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:imeOptions="actionNext"
+ android:inputType="textWebEditText"
+ android:minLines="1" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+</layout>
A src/cheogram/res/layout/command_webview.xml => src/cheogram/res/layout/command_webview.xml +24 -0
@@ 0,0 1,24 @@
+<?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">
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <WebView
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ProgressBar
+ android:id="@+id/progressbar"
+ android:layout_width="match_parent"
+ android:layout_height="130dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingBottom="16dp" />
+
+ </RelativeLayout>
+
+</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/cheogram/res/values/strings.xml => src/cheogram/res/values/strings.xml +6 -0
@@ 13,4 13,10 @@
<string name="if_contact_is_nearby_use_qr">If your contact is nearby, they can also scan the code below to accept your invitation.</string>
<string name="easy_invite_share_text">Join %1$s and chat with me: %2$s</string>
<string name="share_invite_with">Share invite with…</string>
+ <string name="action_cancel">Cancel</string>
+ <string name="action_next">Next</string>
+ <string name="action_prev">Back</string>
+ <string name="action_complete">Finish</string>
+ <string name="action_close">Close</string>
+ <string name="action_execute">Go</string>
</resources>
M src/main/java/eu/siacs/conversations/entities/Contact.java => src/main/java/eu/siacs/conversations/entities/Contact.java +7 -0
@@ 292,6 292,13 @@ public class Contact implements ListItem, Blockable {
return this.presences.getShownStatus();
}
+ public Jid resourceWhichSupport(final String namespace) {
+ final String resource = getPresences().firstWhichSupport(namespace);
+ if (resource == null) return null;
+
+ return resource.equals("") ? getJid() : getJid().withResource(resource);
+ }
+
public boolean setPhotoUri(String uri) {
if (uri != null && !uri.equals(this.photoUri)) {
this.photoUri = uri;
M src/main/java/eu/siacs/conversations/entities/Conversation.java => src/main/java/eu/siacs/conversations/entities/Conversation.java +1249 -1
@@ 1,12 1,48 @@
package eu.siacs.conversations.entities;
+import android.content.ClipData;
+import android.content.ClipboardManager;
import android.content.ContentValues;
import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+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;
+import android.view.MotionEvent;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.AdapterView;
+import android.widget.CompoundButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.Spinner;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.webkit.WebChromeClient;
+import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import androidx.core.content.ContextCompat;
+import androidx.databinding.DataBindingUtil;
+import androidx.databinding.ViewDataBinding;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.viewpager.widget.ViewPager;
+
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.textfield.TextInputLayout;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
@@ 19,20 55,41 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+import java.util.Timer;
+import java.util.TimerTask;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.crypto.PgpDecryptionService;
+import eu.siacs.conversations.databinding.CommandPageBinding;
+import eu.siacs.conversations.databinding.CommandNoteBinding;
+import eu.siacs.conversations.databinding.CommandResultFieldBinding;
+import eu.siacs.conversations.databinding.CommandResultCellBinding;
+import eu.siacs.conversations.databinding.CommandCheckboxFieldBinding;
+import eu.siacs.conversations.databinding.CommandProgressBarBinding;
+import eu.siacs.conversations.databinding.CommandRadioEditFieldBinding;
+import eu.siacs.conversations.databinding.CommandSearchListFieldBinding;
+import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
+import eu.siacs.conversations.databinding.CommandTextFieldBinding;
+import eu.siacs.conversations.databinding.CommandWebviewBinding;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.MessageUtils;
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;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
@@ 84,6 141,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
private String mFirstMamReference = null;
+ protected int mCurrentTab = -1;
+ protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
public Conversation(final String name, final Account account, final Jid contactJid,
final int mode) {
@@ 1109,6 1168,36 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return getName().toString();
}
+ public void setCurrentTab(int tab) {
+ mCurrentTab = tab;
+ }
+
+ public int getCurrentTab() {
+ if (mCurrentTab >= 0) return mCurrentTab;
+
+ if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) {
+ return 0;
+ }
+
+ return 1;
+ }
+
+ public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
+ pagerAdapter.startCommand(command, xmppConnectionService);
+ }
+
+ public void setupViewPager(ViewPager pager, TabLayout tabs) {
+ pagerAdapter.setupViewPager(pager, tabs);
+ }
+
+ public void showViewPager() {
+ pagerAdapter.show();
+ }
+
+ public void hideViewPager() {
+ pagerAdapter.hide();
+ }
+
public interface OnMessageFound {
void onMessageFound(final Message message);
}
@@ 1130,4 1219,1163 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return message;
}
}
+
+ public class ConversationPagerAdapter extends PagerAdapter {
+ protected ViewPager mPager = null;
+ protected TabLayout mTabs = null;
+ ArrayList<CommandSession> sessions = new ArrayList<>();
+
+ public void setupViewPager(ViewPager pager, TabLayout tabs) {
+ mPager = pager;
+ mTabs = tabs;
+ show();
+ pager.setAdapter(this);
+ tabs.setupWithViewPager(mPager);
+ pager.setCurrentItem(getCurrentTab());
+
+ mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ public void onPageScrollStateChanged(int state) { }
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
+
+ public void onPageSelected(int position) {
+ setCurrentTab(position);
+ }
+ });
+ }
+
+ public void show() {
+ if (sessions == null) {
+ sessions = new ArrayList<>();
+ notifyDataSetChanged();
+ }
+ if (mTabs != null) mTabs.setVisibility(View.VISIBLE);
+ }
+
+ public void hide() {
+ if (mPager != null) mPager.setCurrentItem(0);
+ if (mTabs != null) mTabs.setVisibility(View.GONE);
+ sessions = null;
+ notifyDataSetChanged();
+ }
+
+ public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
+ CommandSession session = new CommandSession(command.getAttribute("name"), xmppConnectionService);
+
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ packet.setTo(command.getAttributeAsJid("jid"));
+ final Element c = packet.addChild("command", Namespace.COMMANDS);
+ c.setAttribute("node", command.getAttribute("node"));
+ c.setAttribute("action", "execute");
+ xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
+ mPager.post(() -> {
+ session.updateWithResponse(iq);
+ });
+ });
+
+ sessions.add(session);
+ notifyDataSetChanged();
+ mPager.setCurrentItem(getCount() - 1);
+ }
+
+ public void removeSession(CommandSession session) {
+ sessions.remove(session);
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public Object instantiateItem(@NonNull ViewGroup container, int position) {
+ if (position < 2) {
+ return mPager.getChildAt(position);
+ }
+
+ CommandSession session = sessions.get(position-2);
+ CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_page, container, false);
+ container.addView(binding.getRoot());
+ session.setBinding(binding);
+ return session;
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
+ if (position < 2) return;
+
+ container.removeView(((CommandSession) o).getView());
+ }
+
+ @Override
+ public int getItemPosition(Object o) {
+ if (o == mPager.getChildAt(0)) return PagerAdapter.POSITION_UNCHANGED;
+ if (o == mPager.getChildAt(1)) return PagerAdapter.POSITION_UNCHANGED;
+
+ int pos = sessions.indexOf(o);
+ if (pos < 0) return PagerAdapter.POSITION_NONE;
+ return pos + 2;
+ }
+
+ @Override
+ public int getCount() {
+ if (sessions == null) return 1;
+
+ int count = 2 + sessions.size();
+ if (count > 2) {
+ mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
+ } else {
+ mTabs.setTabMode(TabLayout.MODE_FIXED);
+ }
+ return count;
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
+ if (view == o) return true;
+
+ if (o instanceof CommandSession) {
+ return ((CommandSession) o).getView() == view;
+ }
+
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case 0:
+ return "Conversation";
+ case 1:
+ return "Commands";
+ default:
+ CommandSession session = sessions.get(position-2);
+ if (session == null) return super.getPageTitle(position);
+ return session.getTitle();
+ }
+ }
+
+ class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> {
+ abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
+ protected T binding;
+
+ public ViewHolder(T binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ abstract public void bind(Item el);
+
+ protected void setTextOrHide(TextView v, Optional<String> s) {
+ if (s == null || !s.isPresent()) {
+ v.setVisibility(View.GONE);
+ } else {
+ v.setVisibility(View.VISIBLE);
+ v.setText(s.get());
+ }
+ }
+
+ protected void setupInputType(Element field, TextView textinput, TextInputLayout layout) {
+ int flags = 0;
+ if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_NONE);
+ textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+
+ String type = field.getAttribute("type");
+ if (type != null) {
+ if (type.equals("text-multi") || type.equals("jid-multi")) {
+ flags |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+
+ textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+
+ if (type.equals("jid-single") || type.equals("jid-multi")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ }
+
+ if (type.equals("text-private")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
+ }
+ }
+
+ Element validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
+ if (validate == null) return;
+ String datatype = validate.getAttribute("datatype");
+ if (datatype == null) return;
+
+ if (datatype.equals("xs:integer") || datatype.equals("xs:int") || datatype.equals("xs:long") || datatype.equals("xs:short") || datatype.equals("xs:byte")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
+ }
+
+ if (datatype.equals("xs:decimal") || datatype.equals("xs:double")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL);
+ }
+
+ if (datatype.equals("xs:date")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE);
+ }
+
+ if (datatype.equals("xs:dateTime")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL);
+ }
+
+ if (datatype.equals("xs:time")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME);
+ }
+
+ if (datatype.equals("xs:anyURI")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
+ }
+
+ if (datatype.equals("html:tel")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_PHONE);
+ }
+
+ if (datatype.equals("html:email")) {
+ textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ }
+ }
+ }
+
+ class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
+ public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
+
+ @Override
+ public void bind(Item iq) {
+ binding.errorIcon.setVisibility(View.VISIBLE);
+
+ Element error = iq.el.findChild("error");
+ if (error == null) return;
+ String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
+ if (text == null || text.equals("")) {
+ text = error.getChildren().get(0).getName();
+ }
+ binding.message.setText(text);
+ }
+ }
+
+ class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
+ public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
+
+ @Override
+ public void bind(Item note) {
+ binding.message.setText(note.el.getContent());
+
+ String type = note.el.getAttribute("type");
+ if (type != null && type.equals("error")) {
+ binding.errorIcon.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
+ public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
+
+ @Override
+ public void bind(Item item) {
+ Field field = (Field) item;
+ setTextOrHide(binding.label, field.getLabel());
+ setTextOrHide(binding.desc, field.getDesc());
+
+ ArrayAdapter<String> values = new ArrayAdapter<String>(binding.getRoot().getContext(), R.layout.simple_list_item);
+ for (Element el : field.el.getChildren()) {
+ if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
+ values.add(el.getContent());
+ }
+ }
+ binding.values.setAdapter(values);
+
+ ClipboardManager clipboard = binding.getRoot().getContext().getSystemService(ClipboardManager.class);
+ binding.values.setOnItemLongClickListener((arg0, arg1, pos, id) -> {
+ ClipData myClip = ClipData.newPlainText("text", values.getItem(pos));
+ clipboard.setPrimaryClip(myClip);
+ Toast.makeText(binding.getRoot().getContext(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
+ return true;
+ });
+ }
+ }
+
+ class ResultCellViewHolder extends ViewHolder<CommandResultCellBinding> {
+ public ResultCellViewHolder(CommandResultCellBinding binding) { super(binding); }
+
+ @Override
+ public void bind(Item item) {
+ Cell cell = (Cell) item;
+
+ if (cell.el == null) {
+ binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Subhead);
+ setTextOrHide(binding.text, cell.reported.getLabel());
+ } else {
+ binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Body1);
+ binding.text.setText(cell.el.findChildContent("value", "jabber:x:data"));
+ }
+ }
+ }
+
+ class CheckboxFieldViewHolder extends ViewHolder<CommandCheckboxFieldBinding> implements CompoundButton.OnCheckedChangeListener {
+ public CheckboxFieldViewHolder(CommandCheckboxFieldBinding binding) {
+ super(binding);
+ binding.row.setOnClickListener((v) -> {
+ binding.checkbox.toggle();
+ });
+ binding.checkbox.setOnCheckedChangeListener(this);
+ }
+ protected Element mValue = null;
+
+ @Override
+ public void bind(Item item) {
+ Field field = (Field) item;
+ binding.label.setText(field.getLabel().orElse(""));
+ setTextOrHide(binding.desc, field.getDesc());
+ mValue = field.getValue();
+ binding.checkbox.setChecked(mValue.getContent() != null && (mValue.getContent().equals("true") || mValue.getContent().equals("1")));
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
+ if (mValue == null) return;
+
+ mValue.setContent(isChecked ? "true" : "false");
+ }
+ }
+
+ class SearchListFieldViewHolder extends ViewHolder<CommandSearchListFieldBinding> implements TextWatcher {
+ public SearchListFieldViewHolder(CommandSearchListFieldBinding binding) {
+ super(binding);
+ binding.search.addTextChangedListener(this);
+ }
+ protected Element mValue = null;
+ List<Option> options = new ArrayList<>();
+ protected ArrayAdapter<Option> adapter;
+ protected boolean open;
+
+ @Override
+ public void bind(Item item) {
+ Field field = (Field) item;
+ setTextOrHide(binding.label, field.getLabel());
+ setTextOrHide(binding.desc, field.getDesc());
+
+ if (field.error != null) {
+ binding.desc.setVisibility(View.VISIBLE);
+ binding.desc.setText(field.error);
+ binding.desc.setTextAppearance(R.style.TextAppearance_Conversations_Design_Error);
+ } else {
+ binding.desc.setTextAppearance(R.style.TextAppearance_Conversations_Status);
+ }
+
+ mValue = field.getValue();
+
+ Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
+ open = validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null;
+ setupInputType(field.el, binding.search, null);
+
+ options = field.getOptions();
+ binding.list.setOnItemClickListener((parent, view, position, id) -> {
+ mValue.setContent(adapter.getItem(binding.list.getCheckedItemPosition()).getValue());
+ if (open) binding.search.setText(mValue.getContent());
+ });
+ search("");
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (open) mValue.setContent(s.toString());
+ search(s.toString());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int count, int after) { }
+
+ protected void search(String s) {
+ List<Option> filteredOptions;
+ final String q = s.replaceAll("\\W", "").toLowerCase();
+ if (q == null || q.equals("")) {
+ filteredOptions = options;
+ } else {
+ filteredOptions = options.stream().filter(o -> o.toString().replaceAll("\\W", "").toLowerCase().contains(q)).collect(Collectors.toList());
+ }
+ adapter = new ArrayAdapter(binding.getRoot().getContext(), R.layout.simple_list_item, filteredOptions);
+ binding.list.setAdapter(adapter);
+
+ int checkedPos = filteredOptions.indexOf(new Option(mValue.getContent(), ""));
+ if (checkedPos >= 0) binding.list.setItemChecked(checkedPos, true);
+ }
+ }
+
+ class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener, TextWatcher {
+ public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
+ super(binding);
+ binding.open.addTextChangedListener(this);
+ 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(Item item) {
+ Field field = (Field) item;
+ setTextOrHide(binding.label, field.getLabel());
+ setTextOrHide(binding.desc, field.getDesc());
+
+ if (field.error != null) {
+ binding.desc.setVisibility(View.VISIBLE);
+ binding.desc.setText(field.error);
+ binding.desc.setTextAppearance(R.style.TextAppearance_Conversations_Design_Error);
+ } else {
+ binding.desc.setTextAppearance(R.style.TextAppearance_Conversations_Status);
+ }
+
+ mValue = field.getValue();
+
+ Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
+ binding.open.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
+ binding.open.setText(mValue.getContent());
+ setupInputType(field.el, binding.open, null);
+
+ options.clear();
+ List<Option> theOptions = field.getOptions();
+ 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());
+ binding.open.setText(mValue.getContent());
+ }
+ options.notifyDataSetChanged();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mValue == null) return;
+
+ mValue.setContent(s.toString());
+ options.notifyDataSetChanged();
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int count, int after) { }
+ }
+
+ class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
+ public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
+ super(binding);
+ binding.spinner.setOnItemSelectedListener(this);
+ }
+ protected Element mValue = null;
+
+ @Override
+ public void bind(Item item) {
+ Field field = (Field) item;
+ setTextOrHide(binding.label, field.getLabel());
+ binding.spinner.setPrompt(field.getLabel().orElse(""));
+ setTextOrHide(binding.desc, field.getDesc());
+
+ mValue = field.getValue();
+
+ ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
+ options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ options.addAll(field.getOptions());
+
+ binding.spinner.setAdapter(options);
+ binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ Option o = (Option) parent.getItemAtPosition(pos);
+ if (mValue == null) return;
+
+ mValue.setContent(o == null ? "" : o.getValue());
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ mValue.setContent("");
+ }
+ }
+
+ class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
+ public TextFieldViewHolder(CommandTextFieldBinding binding) {
+ super(binding);
+ binding.textinput.addTextChangedListener(this);
+ }
+ protected Element mValue = null;
+
+ @Override
+ public void bind(Item item) {
+ Field field = (Field) item;
+ binding.textinputLayout.setHint(field.getLabel().orElse(""));
+
+ binding.textinputLayout.setHelperTextEnabled(field.getDesc().isPresent());
+ field.getDesc().ifPresent(binding.textinputLayout::setHelperText);
+
+ binding.textinputLayout.setErrorEnabled(field.error != null);
+ if (field.error != null) binding.textinputLayout.setError(field.error);
+
+ mValue = field.getValue();
+ binding.textinput.setText(mValue.getContent());
+ setupInputType(field.el, binding.textinput, binding.textinputLayout);
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mValue == null) return;
+
+ mValue.setContent(s.toString());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int count, int after) { }
+ }
+
+ class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
+ public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
+
+ @Override
+ public void bind(Item oob) {
+ binding.webview.getSettings().setJavaScriptEnabled(true);
+ binding.webview.getSettings().setUserAgentString("Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36");
+ binding.webview.getSettings().setDatabaseEnabled(true);
+ binding.webview.getSettings().setDomStorageEnabled(true);
+ binding.webview.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ binding.progressbar.setVisibility(newProgress < 100 ? View.VISIBLE : View.GONE);
+ binding.progressbar.setProgress(newProgress);
+ }
+ });
+ binding.webview.setWebViewClient(new WebViewClient() {
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ mTitle = view.getTitle();
+ ConversationPagerAdapter.this.notifyDataSetChanged();
+ }
+ });
+ binding.webview.addJavascriptInterface(new JsObject(), "xmpp_xep0050");
+ binding.webview.loadUrl(oob.el.findChildContent("url", "jabber:x:oob"));
+ }
+
+ class JsObject {
+ @JavascriptInterface
+ public void execute() { execute("execute"); }
+ public void execute(String action) {
+ getView().post(() -> {
+ if(CommandSession.this.execute(action)) {
+ removeSession(CommandSession.this);
+ }
+ });
+ }
+ }
+ }
+
+ class ProgressBarViewHolder extends ViewHolder<CommandProgressBarBinding> {
+ public ProgressBarViewHolder(CommandProgressBarBinding binding) { super(binding); }
+
+ @Override
+ public void bind(Item item) { }
+ }
+
+ class Item {
+ protected Element el;
+ protected int viewType;
+ protected String error = null;
+
+ Item(Element el, int viewType) {
+ this.el = el;
+ this.viewType = viewType;
+ }
+
+ public boolean validate() {
+ error = null;
+ return true;
+ }
+ }
+
+ class Field extends Item {
+ Field(Element el, int viewType) { super(el, viewType); }
+
+ @Override
+ public boolean validate() {
+ if (!super.validate()) return false;
+ if (el.findChild("required", "jabber:x:data") == null) return true;
+ if (getValue().getContent() != null && !getValue().getContent().equals("")) return true;
+
+ error = "this value is required";
+ return false;
+ }
+
+ public String getVar() {
+ return el.getAttribute("var");
+ }
+
+ public Optional<String> getLabel() {
+ String label = el.getAttribute("label");
+ if (label == null) label = getVar();
+ return Optional.ofNullable(label);
+ }
+
+ public Optional<String> getDesc() {
+ return Optional.ofNullable(el.findChildContent("desc", "jabber:x:data"));
+ }
+
+ public Element getValue() {
+ Element value = el.findChild("value", "jabber:x:data");
+ if (value == null) {
+ value = el.addChild("value", "jabber:x:data");
+ }
+ return value;
+ }
+
+ public List<Option> getOptions() {
+ return Option.forField(el);
+ }
+ }
+
+ class Cell extends Item {
+ protected Field reported;
+
+ Cell(Field reported, Element item) {
+ super(item, TYPE_RESULT_CELL);
+ this.reported = reported;
+ }
+ }
+
+ protected Field mkField(Element el) {
+ int viewType = -1;
+
+ String formType = responseElement.getAttribute("type");
+ if (formType != null) {
+ String fieldType = el.getAttribute("type");
+ if (fieldType == null) fieldType = "text-single";
+
+ if (formType.equals("result") || fieldType.equals("fixed")) {
+ viewType = TYPE_RESULT_FIELD;
+ } else if (formType.equals("form")) {
+ if (fieldType.equals("boolean")) {
+ viewType = TYPE_CHECKBOX_FIELD;
+ } else if (fieldType.equals("list-single")) {
+ Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
+ if (Option.forField(el).size() > 9) {
+ viewType = TYPE_SEARCH_LIST_FIELD;
+ } else if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
+ viewType = TYPE_RADIO_EDIT_FIELD;
+ } else {
+ viewType = TYPE_SPINNER_FIELD;
+ }
+ } else {
+ viewType = TYPE_TEXT_FIELD;
+ }
+ }
+
+ return new Field(el, viewType);
+ }
+
+ return null;
+ }
+
+ protected Item mkItem(Element el, int pos) {
+ int viewType = -1;
+
+ if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
+ if (el.getName().equals("note")) {
+ viewType = TYPE_NOTE;
+ } else if (el.getNamespace().equals("jabber:x:oob")) {
+ viewType = TYPE_WEB;
+ } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
+ viewType = TYPE_NOTE;
+ } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
+ Field field = mkField(el);
+ if (field != null) {
+ items.put(pos, field);
+ return field;
+ }
+ }
+ } else if (response != null) {
+ viewType = TYPE_ERROR;
+ }
+
+ Item item = new Item(el, viewType);
+ items.put(pos, item);
+ return item;
+ }
+
+ final int TYPE_ERROR = 1;
+ final int TYPE_NOTE = 2;
+ final int TYPE_WEB = 3;
+ final int TYPE_RESULT_FIELD = 4;
+ 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;
+ final int TYPE_RESULT_CELL = 9;
+ final int TYPE_PROGRESSBAR = 10;
+ final int TYPE_SEARCH_LIST_FIELD = 11;
+
+ protected boolean loading = false;
+ protected Timer loadingTimer = new Timer();
+ protected String mTitle;
+ protected CommandPageBinding mBinding = null;
+ protected IqPacket response = null;
+ protected Element responseElement = null;
+ protected List<Field> reported = null;
+ protected SparseArray<Item> items = new SparseArray<>();
+ protected XmppConnectionService xmppConnectionService;
+ protected ArrayAdapter<String> actionsAdapter;
+ protected GridLayoutManager layoutManager;
+
+ CommandSession(String title, XmppConnectionService xmppConnectionService) {
+ loading();
+ mTitle = title;
+ this.xmppConnectionService = xmppConnectionService;
+ setupLayoutManager();
+ actionsAdapter = new ArrayAdapter<String>(xmppConnectionService, R.layout.simple_list_item) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = super.getView(position, convertView, parent);
+ TextView tv = (TextView) v.findViewById(android.R.id.text1);
+ tv.setGravity(Gravity.CENTER);
+ int resId = xmppConnectionService.getResources().getIdentifier("action_" + tv.getText() , "string" , xmppConnectionService.getPackageName());
+ if (resId != 0) tv.setText(xmppConnectionService.getResources().getString(resId));
+ tv.setTextColor(ContextCompat.getColor(xmppConnectionService, R.color.white));
+ tv.setBackgroundColor(UIHelper.getColorForName(tv.getText().toString()));
+ return v;
+ }
+ };
+ actionsAdapter.registerDataSetObserver(new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ if (mBinding == null) return;
+
+ mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
+ }
+
+ @Override
+ public void onInvalidated() {}
+ });
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public void updateWithResponse(IqPacket iq) {
+ this.loadingTimer.cancel();
+ this.loadingTimer = new Timer();
+ this.loading = false;
+ this.responseElement = null;
+ this.reported = null;
+ this.response = iq;
+ this.items.clear();
+ this.actionsAdapter.clear();
+ layoutManager.setSpanCount(1);
+
+ Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
+ if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
+ for (Element el : command.getChildren()) {
+ if (el.getName().equals("actions") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
+ for (Element action : el.getChildren()) {
+ if (!el.getNamespace().equals("http://jabber.org/protocol/commands")) continue;
+ if (action.getName().equals("execute")) continue;
+
+ actionsAdapter.add(action.getName());
+ }
+ }
+ if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
+ String title = el.findChildContent("title", "jabber:x:data");
+ if (title != null) {
+ mTitle = title;
+ ConversationPagerAdapter.this.notifyDataSetChanged();
+ }
+
+ if (el.getAttribute("type").equals("result") || el.getAttribute("type").equals("form")) {
+ this.responseElement = el;
+ setupReported(el.findChild("reported", "jabber:x:data"));
+ layoutManager.setSpanCount(this.reported == null ? 1 : this.reported.size());
+ }
+ break;
+ }
+ if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
+ String url = el.findChildContent("url", "jabber:x:oob");
+ if (url != null) {
+ String scheme = Uri.parse(url).getScheme();
+ if (scheme.equals("http") || scheme.equals("https")) {
+ this.responseElement = el;
+ break;
+ }
+ }
+ }
+ if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
+ this.responseElement = el;
+ break;
+ }
+ }
+
+ if (responseElement == null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
+ removeSession(this);
+ return;
+ }
+
+ if (command.getAttribute("status").equals("executing") && actionsAdapter.getCount() < 1) {
+ // No actions have been given, but we are not done?
+ // This is probably a spec violation, but we should do *something*
+ actionsAdapter.add("execute");
+ }
+ }
+
+ if (actionsAdapter.getCount() > 0) {
+ if (actionsAdapter.getPosition("cancel") < 0) actionsAdapter.insert("cancel", 0);
+ } else {
+ actionsAdapter.add("close");
+ }
+
+ notifyDataSetChanged();
+ }
+
+ protected void setupReported(Element el) {
+ if (el == null) {
+ reported = null;
+ return;
+ }
+
+ reported = new ArrayList<>();
+ for (Element fieldEl : el.getChildren()) {
+ if (!fieldEl.getName().equals("field") || !fieldEl.getNamespace().equals("jabber:x:data")) continue;
+ reported.add(mkField(fieldEl));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ if (loading) return 1;
+ if (response == null) return 0;
+ if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
+ int i = 0;
+ for (Element el : responseElement.getChildren()) {
+ if (!el.getNamespace().equals("jabber:x:data")) continue;
+ if (el.getName().equals("title")) continue;
+ if (el.getName().equals("field")) {
+ String type = el.getAttribute("type");
+ if (type != null && type.equals("hidden")) continue;
+ }
+
+ if (el.getName().equals("reported") || el.getName().equals("item")) {
+ if (reported != null) i += reported.size();
+ continue;
+ }
+
+ i++;
+ }
+ return i;
+ }
+ return 1;
+ }
+
+ public Item getItem(int position) {
+ if (loading) return new Item(null, TYPE_PROGRESSBAR);
+ if (items.get(position) != null) return items.get(position);
+ if (response == null) return null;
+
+ if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
+ if (responseElement.getNamespace().equals("jabber:x:data")) {
+ int i = 0;
+ for (Element el : responseElement.getChildren()) {
+ if (!el.getNamespace().equals("jabber:x:data")) continue;
+ if (el.getName().equals("title")) continue;
+ if (el.getName().equals("field")) {
+ String type = el.getAttribute("type");
+ if (type != null && type.equals("hidden")) continue;
+ }
+
+ if (el.getName().equals("reported") || el.getName().equals("item")) {
+ Cell cell = null;
+
+ if (reported != null) {
+ if (reported.size() > position - i) {
+ Field reportedField = reported.get(position - i);
+ Element itemField = null;
+ if (el.getName().equals("item")) {
+ for (Element subel : el.getChildren()) {
+ if (subel.getAttribute("var").equals(reportedField.getVar())) {
+ itemField = subel;
+ break;
+ }
+ }
+ }
+ cell = new Cell(reportedField, itemField);
+ } else {
+ i += reported.size();
+ continue;
+ }
+ }
+
+ if (cell != null) {
+ items.put(position, cell);
+ return cell;
+ }
+ }
+
+ if (i < position) {
+ i++;
+ continue;
+ }
+
+ return mkItem(el, position);
+ }
+ }
+ }
+
+ return mkItem(responseElement == null ? response : responseElement, position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return getItem(position).viewType;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
+ switch(viewType) {
+ case TYPE_ERROR: {
+ CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
+ return new ErrorViewHolder(binding);
+ }
+ case TYPE_NOTE: {
+ CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
+ return new NoteViewHolder(binding);
+ }
+ case TYPE_WEB: {
+ CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
+ return new WebViewHolder(binding);
+ }
+ case TYPE_RESULT_FIELD: {
+ CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
+ return new ResultFieldViewHolder(binding);
+ }
+ case TYPE_RESULT_CELL: {
+ CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
+ return new ResultCellViewHolder(binding);
+ }
+ case TYPE_CHECKBOX_FIELD: {
+ CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
+ return new CheckboxFieldViewHolder(binding);
+ }
+ case TYPE_SEARCH_LIST_FIELD: {
+ CommandSearchListFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_search_list_field, container, false);
+ return new SearchListFieldViewHolder(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);
+ }
+ case TYPE_TEXT_FIELD: {
+ CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
+ return new TextFieldViewHolder(binding);
+ }
+ case TYPE_PROGRESSBAR: {
+ CommandProgressBarBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_progress_bar, container, false);
+ return new ProgressBarViewHolder(binding);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown viewType: " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, int position) {
+ viewHolder.bind(getItem(position));
+ }
+
+ public View getView() {
+ return mBinding.getRoot();
+ }
+
+ public boolean validate() {
+ int count = getItemCount();
+ boolean isValid = true;
+ for (int i = 0; i < count; i++) {
+ boolean oneIsValid = getItem(i).validate();
+ isValid = isValid && oneIsValid;
+ }
+ notifyDataSetChanged();
+ return isValid;
+ }
+
+ public boolean execute() {
+ return execute("execute");
+ }
+
+ public boolean execute(int actionPosition) {
+ return execute(actionsAdapter.getItem(actionPosition));
+ }
+
+ public boolean execute(String action) {
+ if (!action.equals("cancel") && !validate()) return false;
+ if (response == null) return true;
+ Element command = response.findChild("command", "http://jabber.org/protocol/commands");
+ if (command == null) return true;
+ String status = command.getAttribute("status");
+ if (status == null || !status.equals("executing")) return true;
+
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ packet.setTo(response.getFrom());
+ final Element c = packet.addChild("command", Namespace.COMMANDS);
+ c.setAttribute("node", command.getAttribute("node"));
+ c.setAttribute("sessionid", command.getAttribute("sessionid"));
+ c.setAttribute("action", action);
+
+ String formType = responseElement == null ? null : responseElement.getAttribute("type");
+ if (!action.equals("cancel") &&
+ responseElement != null &&
+ responseElement.getName().equals("x") &&
+ responseElement.getNamespace().equals("jabber:x:data") &&
+ formType != null && formType.equals("form")) {
+
+ responseElement.setAttribute("type", "submit");
+ Element rsm = responseElement.findChild("set", "http://jabber.org/protocol/rsm");
+ if (rsm != null) {
+ Element max = new Element("max", "http://jabber.org/protocol/rsm");
+ max.setContent("1000");
+ rsm.addChild(max);
+ }
+ c.addChild(responseElement);
+ }
+
+ xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
+ getView().post(() -> {
+ updateWithResponse(iq);
+ });
+ });
+
+ loading();
+ return false;
+ }
+
+ protected void loading() {
+ loadingTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ getView().post(() -> {
+ loading = true;
+ notifyDataSetChanged();
+ });
+ }
+ }, 500);
+ }
+
+ protected GridLayoutManager setupLayoutManager() {
+ layoutManager = new GridLayoutManager(mPager.getContext(), layoutManager == null ? 1 : layoutManager.getSpanCount());
+ layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
+ return 1;
+ }
+ });
+ return layoutManager;
+ }
+
+ public void setBinding(CommandPageBinding b) {
+ mBinding = b;
+ // https://stackoverflow.com/a/32350474/8611
+ mBinding.form.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ if(rv.getChildCount() > 0) {
+ int[] location = new int[2];
+ rv.getLocationOnScreen(location);
+ View childView = rv.findChildViewUnder(e.getX(), e.getY());
+ if (childView instanceof ViewGroup) {
+ childView = findViewAt((ViewGroup) childView, location[0] + e.getX(), location[1] + e.getY());
+ }
+ if (childView instanceof ListView || childView instanceof WebView) {
+ int action = e.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ rv.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallow) { }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) { }
+ });
+ mBinding.form.setLayoutManager(setupLayoutManager());
+ mBinding.form.setAdapter(this);
+ mBinding.actions.setAdapter(actionsAdapter);
+ mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
+ if (execute(pos)) {
+ removeSession(CommandSession.this);
+ }
+ });
+
+ actionsAdapter.notifyDataSetChanged();
+ }
+
+ // https://stackoverflow.com/a/36037991/8611
+ private View findViewAt(ViewGroup viewGroup, float x, float y) {
+ for(int i = 0; i < viewGroup.getChildCount(); i++) {
+ View child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup && !(child instanceof ListView) && !(child instanceof WebView)) {
+ View foundView = findViewAt((ViewGroup) child, x, y);
+ if (foundView != null && foundView.isShown()) {
+ return foundView;
+ }
+ } else {
+ int[] location = new int[2];
+ child.getLocationOnScreen(location);
+ Rect rect = new Rect(location[0], location[1], location[0] + child.getWidth(), location[1] + child.getHeight());
+ if (rect.contains((int)x, (int)y)) {
+ return child;
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+ }
}
M src/main/java/eu/siacs/conversations/entities/IndividualMessage.java => src/main/java/eu/siacs/conversations/entities/IndividualMessage.java +1 -1
@@ 44,7 44,7 @@ public class IndividualMessage extends Message {
}
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean deleted, String bodyLanguage) {
- super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage, null, null, null);
+ super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage, null, null, null, null);
}
@Override
M src/main/java/eu/siacs/conversations/entities/Message.java => src/main/java/eu/siacs/conversations/entities/Message.java +43 -4
@@ 6,6 6,7 @@ import android.graphics.Color;
import android.text.SpannableStringBuilder;
import android.util.Log;
+import com.google.common.io.ByteSource;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Longs;
@@ 13,6 14,7 @@ import com.google.common.primitives.Longs;
import org.json.JSONException;
import java.lang.ref.WeakReference;
+import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
@@ 20,6 22,7 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.Config;
@@ 35,6 38,9 @@ import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Tag;
+import eu.siacs.conversations.xml.XmlReader;
public class Message extends AbstractEntity implements AvatarService.Avatarable {
@@ 107,6 113,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
protected boolean carbon = false;
private boolean oob = false;
protected URI oobUri = null;
+ protected List<Element> payloads = new ArrayList<>();
protected List<Edit> edits = new ArrayList<>();
protected String relativeFilePath;
protected boolean read = true;
@@ 161,6 168,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
null,
null,
null,
+ null,
null);
}
@@ 189,6 197,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
null,
null,
null,
+ null,
null);
}
@@ 198,7 207,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint, final boolean read,
final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
- final boolean markable, final boolean deleted, final String bodyLanguage, final String subject, final String oobUri, final String fileParams) {
+ final boolean markable, final boolean deleted, final String bodyLanguage, final String subject, final String oobUri, final String fileParams, final List<Element> payloads) {
this.conversation = conversation;
this.uuid = uuid;
this.conversationUuid = conversationUUid;
@@ 225,9 234,21 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.bodyLanguage = bodyLanguage;
this.subject = subject;
if (fileParams != null) this.fileParams = new FileParams(fileParams);
- }
+ if (payloads != null) this.payloads = payloads;
+ }
+
+ public static Message fromCursor(Cursor cursor, Conversation conversation) throws IOException {
+ String payloadsStr = cursor.getString(cursor.getColumnIndex("payloads"));
+ List<Element> payloads = new ArrayList<>();
+ if (payloadsStr != null) {
+ final XmlReader xmlReader = new XmlReader();
+ xmlReader.setInputStream(ByteSource.wrap(payloadsStr.getBytes()).openStream());
+ Tag tag;
+ while ((tag = xmlReader.readTag()) != null) {
+ payloads.add(xmlReader.readElement(tag));
+ }
+ }
- public static Message fromCursor(Cursor cursor, Conversation conversation) {
return new Message(conversation,
cursor.getString(cursor.getColumnIndex(UUID)),
cursor.getString(cursor.getColumnIndex(CONVERSATION)),
@@ 253,7 274,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
cursor.getString(cursor.getColumnIndex("subject")),
cursor.getString(cursor.getColumnIndex("oobUri")),
- cursor.getString(cursor.getColumnIndex("fileParams"))
+ cursor.getString(cursor.getColumnIndex("fileParams")),
+ payloads
);
}
@@ 289,6 311,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
values.put("subject", subject);
values.put("oobUri", oobUri == null ? null : oobUri.toString());
values.put("fileParams", fileParams == null ? null : fileParams.toString());
+ values.put("payloads", payloads.size() < 1 ? null : payloads.stream().map(Object::toString).collect(Collectors.joining()));
return values;
}
@@ 841,6 864,22 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.oob = this.oobUri != null;
}
+ public void addPayload(Element el) {
+ this.payloads.add(el);
+ }
+
+ public List<Element> getCommands() {
+ if (this.payloads == null) return null;
+
+ for (Element el : this.payloads) {
+ if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
+ return el.getChildren();
+ }
+ }
+
+ return null;
+ }
+
public String getMimeType() {
String extension;
if (relativeFilePath != null) {
M src/main/java/eu/siacs/conversations/entities/Presences.java => src/main/java/eu/siacs/conversations/entities/Presences.java +13 -0
@@ 149,6 149,19 @@ public class Presences {
return false;
}
+ public String firstWhichSupport(final String namespace) {
+ for (Map.Entry<String, Presence> entry : this.presences.entrySet()) {
+ String resource = entry.getKey();
+ Presence presence = entry.getValue();
+ ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
+ if (disco != null && disco.getFeatures().contains(namespace)) {
+ return resource;
+ }
+ }
+
+ return null;
+ }
+
public boolean anyIdentity(final String category, final String type) {
synchronized (this.presences) {
if (this.presences.size() == 0) {
M src/main/java/eu/siacs/conversations/generator/IqGenerator.java => src/main/java/eu/siacs/conversations/generator/IqGenerator.java +8 -1
@@ 552,7 552,14 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket queryDiscoItems(Jid jid) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(jid);
- packet.addChild("query",Namespace.DISCO_ITEMS);
+ packet.query(Namespace.DISCO_ITEMS);
+ return packet;
+ }
+
+ public IqPacket queryDiscoItems(Jid jid, String node) {
+ IqPacket packet = queryDiscoItems(jid);
+ final Element query = packet.query(Namespace.DISCO_ITEMS);
+ query.setAttribute("node", node);
return packet;
}
M src/main/java/eu/siacs/conversations/parser/MessageParser.java => src/main/java/eu/siacs/conversations/parser/MessageParser.java +5 -0
@@ 585,6 585,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
+ for (Element el : packet.getChildren()) {
+ if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
+ message.addPayload(el);
+ }
+ }
if (conversationMultiMode) {
message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
M src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java => src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +8 -0
@@ 244,6 244,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("PRAGMA cheogram.user_version = 2");
}
+ if(cheogramVersion < 3) {
+ db.execSQL(
+ "ALTER TABLE cheogram." + Message.TABLENAME + " " +
+ "ADD COLUMN payloads TEXT"
+ );
+ db.execSQL("PRAGMA cheogram.user_version = 3");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +25 -7
@@ 4673,6 4673,7 @@ public class XmppConnectionService extends Service {
if (result != null) {
return result;
} else {
+ if (key.first == null || key.second == null) return null;
result = databaseBackend.findDiscoveryResult(key.first, key.second);
if (result != null) {
discoCache.put(key, result);
@@ 4700,6 4701,10 @@ public class XmppConnectionService extends Service {
}
public void fetchCaps(Account account, final Jid jid, final Presence presence) {
+ fetchCaps(account, jid, presence, null);
+ }
+
+ public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
@@ 4726,14 4731,15 @@ public class XmppConnectionService extends Service {
sendIqPacket(account, request, (a, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
final ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
- if (presence.getVer().equals(discoveryResult.getVer())) {
+ if (presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
databaseBackend.insertDiscoveryResult(discoveryResult);
- injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult);
+ injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), jid.getResource(), discoveryResult);
if (discoveryResult.hasIdentity("gateway", "pstn")) {
final Contact contact = account.getRoster().getContact(jid);
contact.registerAsPhoneAccount(this);
mQuickConversationsService.considerSyncBackground(false);
}
+ if (cb != null) cb.run();
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer());
}
@@ 4744,14 4750,26 @@ public class XmppConnectionService extends Service {
}
}
- private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
+ public void fetchCommands(Account account, final Jid jid, OnIqPacketReceived callback) {
+ final IqPacket request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
+ sendIqPacket(account, request, callback);
+ }
+
+ private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) {
boolean rosterNeedsSync = false;
for (final Contact contact : roster.getContacts()) {
boolean serviceDiscoverySet = false;
- for (final Presence presence : contact.getPresences().getPresences()) {
- if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
- presence.setServiceDiscoveryResult(disco);
- serviceDiscoverySet = true;
+ Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
+ if (onePresence != null) {
+ onePresence.setServiceDiscoveryResult(disco);
+ serviceDiscoverySet = true;
+ }
+ if (hash != null && ver != null) {
+ for (final Presence presence : contact.getPresences().getPresences()) {
+ if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
+ presence.setServiceDiscoveryResult(disco);
+ serviceDiscoverySet = true;
+ }
}
}
if (serviceDiscoverySet) {
M src/main/java/eu/siacs/conversations/ui/ConversationFragment.java => src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +62 -0
@@ 55,11 55,14 @@ import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.databinding.DataBindingUtil;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
@@ 73,6 76,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
@@ 92,6 96,7 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
@@ 100,6 105,7 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.adapter.CommandAdapter;
import eu.siacs.conversations.ui.adapter.MediaPreviewAdapter;
import eu.siacs.conversations.ui.adapter.MessageAdapter;
import eu.siacs.conversations.ui.util.ActivityResult;
@@ 129,6 135,7 @@ import eu.siacs.conversations.utils.QuickLoader;
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
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.XmppConnection;
@@ 139,6 146,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class ConversationFragment extends XmppFragment
implements EditMessage.KeyboardListener,
@@ 185,6 193,7 @@ public class ConversationFragment extends XmppFragment
private final PendingItem<Message> pendingMessage = new PendingItem<>();
public Uri mPendingEditorContent = null;
protected MessageAdapter messageListAdapter;
+ protected CommandAdapter commandAdapter;
private MediaPreviewAdapter mediaPreviewAdapter;
private String lastMessageUuid = null;
private Conversation conversation;
@@ 1518,6 1527,9 @@ public class ConversationFragment extends XmppFragment
case R.id.action_toggle_pinned:
togglePinned();
break;
+ case R.id.action_refresh_feature_discovery:
+ refreshFeatureDiscovery();
+ break;
default:
break;
}
@@ 1557,6 1569,17 @@ public class ConversationFragment extends XmppFragment
}
}
+ private void refreshFeatureDiscovery() {
+ for (Map.Entry<String, Presence> entry : conversation.getContact().getPresences().getPresencesMap().entrySet()) {
+ Jid jid = conversation.getContact().getJid();
+ if (!entry.getKey().equals("")) jid = jid.withResource(entry.getKey());
+ activity.xmppConnectionService.fetchCaps(conversation.getAccount(), jid, entry.getValue(), () -> {
+ if (activity == null) return;
+ activity.runOnUiThread(() -> { refresh(); });
+ });
+ }
+ }
+
private void togglePinned() {
final boolean pinned =
conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false);
@@ 2493,9 2516,47 @@ public class ConversationFragment extends XmppFragment
activity.xmppConnectionService
.getNotificationService()
.setOpenConversation(this.conversation);
+
+ if (commandAdapter == null) {
+ conversation.setupViewPager(binding.conversationViewPager, binding.tabLayout);
+ commandAdapter = new CommandAdapter((XmppActivity) getActivity());
+ binding.commandsView.setAdapter(commandAdapter);
+ binding.commandsView.setOnItemClickListener((parent, view, position, id) -> {
+ conversation.startCommand(commandAdapter.getItem(position), activity.xmppConnectionService);
+ });
+ refreshCommands();
+ }
+
return true;
}
+ protected void refreshCommands() {
+ if (commandAdapter == null) return;
+
+ Jid commandJid = conversation.getContact().resourceWhichSupport(Namespace.COMMANDS);
+ if (commandJid == null) {
+ conversation.hideViewPager();
+ } else {
+ conversation.showViewPager();
+ activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (a, iq) -> {
+ if (activity == null) return;
+
+ activity.runOnUiThread(() -> {
+ if (iq.getType() == IqPacket.TYPE.RESULT) {
+ binding.commandsViewProgressbar.setVisibility(View.GONE);
+ commandAdapter.clear();
+ for (Element child : iq.query().getChildren()) {
+ if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue;
+ commandAdapter.add(child);
+ }
+ }
+
+ if (commandAdapter.getCount() < 1) conversation.hideViewPager();
+ });
+ });
+ }
+ }
+
private void resetUnreadMessagesCount() {
lastMessageUuid = null;
hideUnreadMessagesCount();
@@ 2794,6 2855,7 @@ public class ConversationFragment extends XmppFragment
}
updateSendButton();
updateEditablity();
+ refreshCommands();
}
}
}
A src/main/java/eu/siacs/conversations/ui/adapter/CommandAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/CommandAdapter.java +27 -0
@@ 0,0 1,27 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.databinding.CommandRowBinding;
+
+public class CommandAdapter extends ArrayAdapter<Element> {
+ public CommandAdapter(XmppActivity activity) {
+ super(activity, 0);
+ }
+
+ @Override
+ public View getView(int position, View view, @NonNull ViewGroup parent) {
+ CommandRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.command_row, parent, false);
+ binding.command.setText(getItem(position).getAttribute("name"));
+ return binding.getRoot();
+ }
+}
A src/main/java/eu/siacs/conversations/ui/adapter/CommandButtonAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/CommandButtonAdapter.java +29 -0
@@ 0,0 1,29 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.databinding.CommandButtonBinding;
+
+public class CommandButtonAdapter extends ArrayAdapter<Element> {
+ public CommandButtonAdapter(XmppActivity activity) {
+ super(activity, 0);
+ }
+
+ @Override
+ public View getView(int position, View view, @NonNull ViewGroup parent) {
+ CommandButtonBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.command_button, parent, false);
+ binding.command.setText(getItem(position).getAttribute("name"));
+ binding.command.setFocusable(false);
+ binding.command.setClickable(false);
+ return binding.getRoot();
+ }
+}
M src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +15 -0
@@ 24,6 24,7 @@ import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
@@ 75,6 76,7 @@ import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.mam.MamReference;
+import eu.siacs.conversations.xml.Element;
public class MessageAdapter extends ArrayAdapter<Message> {
@@ 616,6 618,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
final Conversational conversation = message.getConversation();
final Account account = conversation.getAccount();
+ final List<Element> commands = message.getCommands();
final int type = getItemViewType(position);
ViewHolder viewHolder;
if (view == null) {
@@ 661,6 664,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.encryption = view.findViewById(R.id.message_encryption);
viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
+ viewHolder.commands_list = view.findViewById(R.id.commands_list);
break;
case STATUS:
view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
@@ 830,6 834,16 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
if (type == RECEIVED) {
+ if (commands != null && conversation instanceof Conversation) {
+ CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
+ adapter.addAll(commands);
+ viewHolder.commands_list.setAdapter(adapter);
+ viewHolder.commands_list.setVisibility(View.VISIBLE);
+ viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> {
+ ((Conversation) conversation).startCommand(adapter.getItem(pos), activity.xmppConnectionService);
+ });
+ }
+
if (isInValidSession) {
int bubble;
if (!mUseGreenBackground) {
@@ 938,5 952,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected ImageView contact_picture;
protected TextView status_message;
protected TextView encryption;
+ protected ListView commands_list;
}
}
A src/main/res/layout/command_row.xml => src/main/res/layout/command_row.xml +36 -0
@@ 0,0 1,36 @@
+<?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">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?selectableItemBackground"
+ android:paddingLeft="8dp"
+ android:paddingBottom="8dp"
+ android:paddingTop="8dp">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/avatar_item_distance">
+
+ <TextView
+ android:id="@+id/command"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scrollHorizontally="false"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Conversations.Body2" />
+ </LinearLayout>
+
+ </RelativeLayout>
+</layout>
M src/main/res/layout/fragment_conversation.xml => src/main/res/layout/fragment_conversation.xml +200 -148
@@ 7,164 7,216 @@
android:layout_height="match_parent"
android:background="?attr/color_background_secondary">
- <ListView
- android:id="@+id/messages_view"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_above="@+id/snackbar"
- android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
- android:layout_alignParentTop="true"
- android:background="?attr/color_background_secondary"
- android:divider="@null"
- android:dividerHeight="0dp"
- android:listSelector="@android:color/transparent"
- android:stackFromBottom="true"
- android:transcriptMode="normal"
- tools:listitem="@layout/message_sent"></ListView>
-
- <com.google.android.material.floatingactionbutton.FloatingActionButton
- android:id="@+id/scroll_to_bottom_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBottom="@+id/messages_view"
- android:layout_alignParentEnd="true"
- android:layout_alignParentRight="true"
- android:alpha="0.85"
- android:src="?attr/icon_scroll_down"
+ <com.google.android.material.tabs.TabLayout
android:visibility="gone"
- app:backgroundTint="?attr/color_background_primary"
- app:fabSize="mini"
- app:useCompatPadding="true" />
-
- <eu.siacs.conversations.ui.widget.UnreadCountCustomView
- android:id="@+id/unread_count_custom_view"
- android:layout_width="?attr/IconSize"
- android:layout_height="?attr/IconSize"
- android:layout_alignTop="@+id/scroll_to_bottom_button"
- android:layout_alignEnd="@+id/scroll_to_bottom_button"
- android:layout_alignRight="@+id/scroll_to_bottom_button"
- android:layout_marginTop="16dp"
- android:layout_marginEnd="8dp"
- android:layout_marginRight="8dp"
- android:elevation="8dp"
- android:visibility="gone"
- app:backgroundColor="?attr/unread_count"
- tools:ignore="RtlCompat" />
-
- <RelativeLayout
- android:id="@+id/textsend"
- android:layout_width="fill_parent"
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
- android:layout_alignParentBottom="true"
+ android:background="?attr/colorPrimary"
+ android:elevation="@dimen/toolbar_elevation"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ app:tabGravity="fill"
+ app:tabIndicatorColor="@color/white87"
+ app:tabMode="scrollable"
+ app:tabSelectedTextColor="@color/white"
+ app:tabTextColor="@color/white70" />
+
+ <androidx.viewpager.widget.ViewPager
+ android:id="@+id/conversation_view_pager"
+ android:layout_below="@id/tab_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
android:background="?attr/color_background_primary">
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
- android:layout_toStartOf="@+id/textSendButton"
- android:layout_toLeftOf="@+id/textSendButton"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/text_input_hint"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- android:maxLines="1"
- android:paddingLeft="8dp"
- android:paddingRight="8dp"
- android:textAppearance="@style/TextAppearance.Conversations.Caption.Highlight"
- android:visibility="gone" />
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/media_preview"
+ <ListView
+ android:id="@+id/messages_view"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/snackbar"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="?attr/color_background_secondary"
+ android:divider="@null"
+ android:dividerHeight="0dp"
+ android:listSelector="@android:color/transparent"
+ android:stackFromBottom="true"
+ android:transcriptMode="normal"
+ tools:listitem="@layout/message_sent"></ListView>
+
+ <RelativeLayout
+ android:id="@+id/textsend"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:background="?attr/color_background_primary">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_toStartOf="@+id/textSendButton"
+ android:layout_toLeftOf="@+id/textSendButton"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/text_input_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:maxLines="1"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:textAppearance="@style/TextAppearance.Conversations.Caption.Highlight"
+ android:visibility="gone" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/media_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingTop="8dp"
+ android:requiresFadingEdge="horizontal"
+ android:visibility="gone"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:listitem="@layout/media_preview">
+
+ </androidx.recyclerview.widget.RecyclerView>
+
+ <eu.siacs.conversations.ui.widget.EditMessage
+ android:id="@+id/textinput"
+ style="@style/Widget.Conversations.EditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/color_background_primary"
+ android:ems="10"
+ android:imeOptions="flagNoExtractUi|actionSend"
+ android:inputType="textShortMessage|textMultiLine|textCapSentences"
+ android:maxLines="8"
+ android:minHeight="48dp"
+ android:minLines="1"
+ android:padding="8dp">
+
+ <requestFocus />
+ </eu.siacs.conversations.ui.widget.EditMessage>
+
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/textSendButton"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?attr/color_background_primary"
+ android:contentDescription="@string/send_message"
+ android:src="?attr/ic_send_text_offline" />
+ </RelativeLayout>
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/scroll_to_bottom_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:paddingTop="8dp"
- android:requiresFadingEdge="horizontal"
+ android:layout_alignBottom="@+id/messages_view"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:alpha="0.85"
+ android:src="?attr/icon_scroll_down"
android:visibility="gone"
- app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
- tools:listitem="@layout/media_preview">
-
- </androidx.recyclerview.widget.RecyclerView>
+ app:backgroundTint="?attr/color_background_primary"
+ app:fabSize="mini"
+ app:useCompatPadding="true" />
+
+ <eu.siacs.conversations.ui.widget.UnreadCountCustomView
+ android:id="@+id/unread_count_custom_view"
+ android:layout_width="?attr/IconSize"
+ android:layout_height="?attr/IconSize"
+ android:layout_alignTop="@+id/scroll_to_bottom_button"
+ android:layout_alignEnd="@+id/scroll_to_bottom_button"
+ android:layout_alignRight="@+id/scroll_to_bottom_button"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginRight="8dp"
+ android:elevation="8dp"
+ android:visibility="gone"
+ app:backgroundColor="?attr/unread_count"
+ tools:ignore="RtlCompat" />
- <eu.siacs.conversations.ui.widget.EditMessage
- android:id="@+id/textinput"
- style="@style/Widget.Conversations.EditText"
- android:layout_width="match_parent"
+ <RelativeLayout
+ android:id="@+id/snackbar"
+ android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:background="?attr/color_background_primary"
- android:ems="10"
- android:imeOptions="flagNoExtractUi|actionSend"
- android:inputType="textShortMessage|textMultiLine|textCapSentences"
- android:maxLines="8"
+ android:layout_above="@+id/textsend"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginBottom="4dp"
+ android:background="@drawable/snackbar"
android:minHeight="48dp"
- android:minLines="1"
- android:padding="8dp">
-
- <requestFocus />
- </eu.siacs.conversations.ui.widget.EditMessage>
-
- </LinearLayout>
-
- <ImageButton
- android:id="@+id/textSendButton"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_alignParentEnd="true"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:background="?attr/color_background_primary"
- android:contentDescription="@string/send_message"
- android:src="?attr/ic_send_text_offline" />
- </RelativeLayout>
-
- <RelativeLayout
- android:id="@+id/snackbar"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_above="@+id/textsend"
- android:layout_marginLeft="8dp"
- android:layout_marginRight="8dp"
- android:layout_marginBottom="4dp"
- android:background="@drawable/snackbar"
- android:minHeight="48dp"
- android:visibility="gone">
-
- <TextView
- android:id="@+id/snackbar_message"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
- android:layout_centerVertical="true"
- android:layout_toStartOf="@+id/snackbar_action"
- android:layout_toLeftOf="@+id/snackbar_action"
- android:paddingStart="24dp"
- android:paddingLeft="24dp"
- android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark" />
-
- <TextView
- android:id="@+id/snackbar_action"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentEnd="true"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:paddingLeft="24dp"
- android:paddingTop="16dp"
- android:paddingRight="24dp"
- android:paddingBottom="16dp"
- android:textAllCaps="true"
- android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark"
- android:textStyle="bold" />
- </RelativeLayout>
+ android:visibility="gone">
+
+ <TextView
+ android:id="@+id/snackbar_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:layout_toStartOf="@+id/snackbar_action"
+ android:layout_toLeftOf="@+id/snackbar_action"
+ android:paddingStart="24dp"
+ android:paddingLeft="24dp"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark" />
+
+ <TextView
+ android:id="@+id/snackbar_action"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:paddingLeft="24dp"
+ android:paddingTop="16dp"
+ android:paddingRight="24dp"
+ android:paddingBottom="16dp"
+ android:textAllCaps="true"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark"
+ android:textStyle="bold" />
+ </RelativeLayout>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <ListView
+ android:id="@+id/commands_view"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="?attr/color_background_secondary"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp"></ListView>
+
+ <ProgressBar
+ android:id="@+id/commands_view_progressbar"
+ android:layout_width="match_parent"
+ android:layout_height="130dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingBottom="16dp" />
+
+ </RelativeLayout>
+
+ </androidx.viewpager.widget.ViewPager>
</RelativeLayout>
-</layout>>
\ No newline at end of file
+</layout>
M src/main/res/layout/message_content.xml => src/main/res/layout/message_content.xml +9 -1
@@ 28,6 28,14 @@
android:longClickable="true"
android:visibility="gone"/>
+ <ListView
+ android:id="@+id/commands_list"
+ android:visibility="gone"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp"></ListView>
+
<RelativeLayout
android:id="@+id/audio_player"
android:layout_width="@dimen/audio_player_width"
@@ 63,4 71,4 @@
android:progress="100"/>
</RelativeLayout>
-</merge>>
\ No newline at end of file
+</merge>
M src/main/res/layout/simple_list_item.xml => src/main/res/layout/simple_list_item.xml +2 -1
@@ 22,4 22,5 @@
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textAppearance="@style/TextAppearance.Conversations.Body1"
- android:textColor="?attr/edit_text_color" />
+ android:textColor="?attr/edit_text_color"
+ android:background="@drawable/list_choice" />
M src/main/res/menu/fragment_conversation.xml => src/main/res/menu/fragment_conversation.xml +7 -1
@@ 135,8 135,14 @@
android:orderInCategory="73"
android:title="@string/add_to_favorites"
app:showAsAction="never" />
+
+ <item
+ android:id="@+id/action_refresh_feature_discovery"
+ android:orderInCategory="74"
+ android:title="Refresh Feature Discovery"
+ app:showAsAction="never" />
</menu>
</item>
-</menu>>
\ No newline at end of file
+</menu>