commands tab in conversation

This commit is contained in:
kosyak 2023-10-25 23:38:54 +02:00
parent 43870114d9
commit f2012bc7f5
43 changed files with 3434 additions and 222 deletions

View file

@ -91,6 +91,8 @@ dependencies {
implementation 'com.splitwise:tokenautocomplete:3.0.2'
implementation 'com.github.kizitonwose.colorpreference:support:1.1.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4'
}
ext {

View file

@ -294,6 +294,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;

File diff suppressed because it is too large Load diff

View file

@ -48,7 +48,7 @@ public class Presence implements Comparable<Presence> {
private final String node;
private final String message;
private Presence(Status status, String ver, String hash, String node, String message) {
public Presence(Status status, String ver, String hash, String node, String message) {
this.status = status;
this.ver = ver;
this.hash = hash;

View file

@ -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) {

View file

@ -564,7 +564,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;
}

View file

@ -2,14 +2,19 @@ package eu.siacs.conversations.persistance;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageDecoder;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.pdf.PdfRenderer;
import android.media.MediaMetadataRetriever;
import android.media.MediaScannerConnection;
@ -32,6 +37,8 @@ import androidx.annotation.StringRes;
import androidx.core.content.FileProvider;
import androidx.exifinterface.media.ExifInterface;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
@ -987,6 +994,108 @@ public class FileBackend {
return thumbnail;
}
public Drawable getThumbnail(DownloadableFile file, Resources res, int size, boolean cacheOnly) throws IOException {
return getThumbnail(file, res, size, cacheOnly, file.getAbsolutePath());
}
public Drawable getThumbnail(DownloadableFile file, Resources res, int size, boolean cacheOnly, String cacheKey) throws IOException {
final LruCache<String, Drawable> cache = mXmppConnectionService.getDrawableCache();
Drawable thumbnail = cache.get(cacheKey);
if ((thumbnail == null) && (!cacheOnly) && file.exists()) {
synchronized (THUMBNAIL_LOCK) {
thumbnail = cache.get(cacheKey);
if (thumbnail != null) {
return thumbnail;
}
final String mime = file.getMimeType();
if ("image/svg+xml".equals(mime)) {
thumbnail = getSVG(file, size);
} else if ("application/pdf".equals(mime)) {
thumbnail = new BitmapDrawable(res, getPdfDocumentPreview(file, size));
} else if (mime.startsWith("video/")) {
thumbnail = new BitmapDrawable(res, getVideoPreview(file, size));
} else {
thumbnail = getImagePreview(file, res, size, mime);
if (thumbnail == null) {
throw new FileNotFoundException();
}
}
if (cacheKey != null && thumbnail != null) cache.put(cacheKey, thumbnail);
}
}
return thumbnail;
}
public Drawable getSVG(File file, int size) {
try {
SVG svg = SVG.getFromInputStream(new FileInputStream(file));
svg.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
float w = svg.getDocumentWidth();
float h = svg.getDocumentHeight();
Rect r = rectForSize((int) w, (int) h, size);
svg.setDocumentWidth("100%");
svg.setDocumentHeight("100%");
Bitmap output = Bitmap.createBitmap(r.width(), r.height(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
svg.renderToCanvas(canvas);
return new SVGDrawable(output);
} catch (final IOException | SVGParseException e) {
return null;
}
}
public static Rect rectForSize(int w, int h, int size) {
int scalledW;
int scalledH;
if (w <= h) {
scalledW = Math.max((int) (w / ((double) h / size)), 1);
scalledH = size;
} else {
scalledW = size;
scalledH = Math.max((int) (h / ((double) w / size)), 1);
}
if (scalledW > w || scalledH > h) return new Rect(0, 0, w, h);
return new Rect(0, 0, scalledW, scalledH);
}
private Drawable getImagePreview(File file, Resources res, int size, final String mime) throws IOException {
if (android.os.Build.VERSION.SDK_INT >= 28) {
ImageDecoder.Source source = ImageDecoder.createSource(file);
return ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
int w = info.getSize().getWidth();
int h = info.getSize().getHeight();
Rect r = rectForSize(w, h, size);
decoder.setTargetSize(r.width(), r.height());
});
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(file, size);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
} catch (OutOfMemoryError e) {
options.inSampleSize *= 2;
bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
}
if (bitmap == null) return null;
bitmap = resize(bitmap, size);
bitmap = rotate(bitmap, getRotation(file));
if (mime.equals("image/gif")) {
Bitmap withGifOverlay = bitmap.copy(Bitmap.Config.ARGB_8888, true);
drawOverlay(withGifOverlay, paintOverlayBlack(withGifOverlay) ? R.drawable.play_gif_black : R.drawable.play_gif_white, 1.0f);
bitmap.recycle();
bitmap = withGifOverlay;
}
return new BitmapDrawable(res, bitmap);
}
}
private Bitmap getFullSizeImagePreview(File file, int size) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(file, size);
@ -1700,4 +1809,8 @@ public class FileBackend {
return resId;
}
}
public static class SVGDrawable extends BitmapDrawable {
public SVGDrawable(Bitmap bm) { super(bm); }
}
}

View file

@ -21,6 +21,8 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.Network;
@ -489,6 +491,7 @@ public class XmppConnectionService extends Service {
private PgpEngine mPgpEngine = null;
private WakeLock wakeLock;
private LruCache<String, Bitmap> mBitmapCache;
private LruCache<String, Drawable> mDrawableCache;
private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
@ -1187,6 +1190,21 @@ public class XmppConnectionService extends Service {
return bitmap.getByteCount() / 1024;
}
};
this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
@Override
protected int sizeOf(final String key, final Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap == null) return 1024;
return bitmap.getByteCount() / 1024;
} else {
return drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 40 / 1024;
}
}
};
if (mLastActivity == 0) {
mLastActivity = getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
}
@ -4285,7 +4303,7 @@ public class XmppConnectionService extends Service {
}
}
private SharedPreferences getPreferences() {
public SharedPreferences getPreferences() {
return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
}
@ -4536,6 +4554,10 @@ public class XmppConnectionService extends Service {
return this.mBitmapCache;
}
public LruCache<String, Drawable> getDrawableCache() {
return this.mDrawableCache;
}
public Collection<String> getKnownHosts() {
final Set<String> hosts = new HashSet<>();
for (final Account account : getAccounts()) {
@ -4601,9 +4623,13 @@ public class XmppConnectionService extends Service {
}
public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
sendIqPacket(account, packet, callback, null);
}
public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback, Long timeout) {
final XmppConnection connection = account.getXmppConnection();
if (connection != null) {
connection.sendIqPacket(packet, callback);
connection.sendIqPacket(packet, callback, timeout);
} else if (callback != null) {
callback.onIqPacketReceived(account, new IqPacket(IqPacket.TYPE.TIMEOUT));
}
@ -4855,8 +4881,13 @@ public class XmppConnectionService extends Service {
}
public void fetchCaps(Account account, final Jid jid, final Presence presence) {
final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
fetchCaps(account, jid, presence, null);
}
public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
final Pair<String, String> key = presence == null ? null : new Pair<>(presence.getHash(), presence.getVer());
final ServiceDiscoveryResult disco = key == null ? null : getCachedServiceDiscoveryResult(key);
if (disco != null) {
presence.setServiceDiscoveryResult(disco);
final Contact contact = account.getRoster().getContact(jid);
@ -4866,19 +4897,21 @@ public class XmppConnectionService extends Service {
} else {
final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
request.setTo(jid);
final String node = presence.getNode();
final String ver = presence.getVer();
final String node = presence == null ? null : presence.getNode();
final String ver = presence == null ? null : presence.getVer();
final Element query = request.query(Namespace.DISCO_INFO);
if (node != null && ver != null) {
query.setAttribute("node", node + "#" + ver);
}
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + (key == null ? "" : key.second) + " to " + jid);
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 == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
databaseBackend.insertDiscoveryResult(discoveryResult);
injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult);
injectServiceDiscoveryResult(a.getRoster(), presence == null ? null : presence.getHash(), presence == null ? null : presence.getVer(), jid.getResource(), discoveryResult);
if (cb != null) cb.run();
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer());
}
@ -4889,16 +4922,32 @@ 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, 5000L);
}
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;
Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
if (onePresence != null) {
onePresence.setServiceDiscoveryResult(disco);
serviceDiscoverySet = true;
} else if (resource == null && hash == null && ver == null) {
Presence p = new Presence(Presence.Status.OFFLINE, null, null, null, "");
p.setServiceDiscoveryResult(disco);
contact.updatePresence("", p);
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) {
rosterNeedsSync |= contact.refreshRtpCapability();
}

View file

@ -49,6 +49,7 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
@ -70,12 +71,14 @@ import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.databinding.DataBindingUtil;
import androidx.viewpager.widget.PagerAdapter;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -84,6 +87,7 @@ import java.util.Comparator;
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;
@ -112,6 +116,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;
@ -130,6 +135,7 @@ import eu.siacs.conversations.ui.util.SendButtonTool;
import eu.siacs.conversations.ui.util.ShareUtil;
import eu.siacs.conversations.ui.util.ViewUtil;
import eu.siacs.conversations.ui.widget.EditMessage;
import eu.siacs.conversations.ui.widget.TabLayout;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
@ -141,6 +147,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;
@ -151,6 +158,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,
@ -199,6 +207,7 @@ public class ConversationFragment extends XmppFragment
public Uri mPendingEditorContent = null;
protected MessageAdapter messageListAdapter;
private MediaPreviewAdapter mediaPreviewAdapter;
protected CommandAdapter commandAdapter;
private String lastMessageUuid = null;
private Conversation conversation;
private FragmentConversationBinding binding;
@ -650,6 +659,13 @@ public class ConversationFragment extends XmppFragment
}
};
private TabLayout.VisibilityChangeListener visibiltyChangeListener = new TabLayout.VisibilityChangeListener() {
@Override
public void onVisibilityChanged(int visibility) {
applyTabElevationFix(visibility == View.VISIBLE);
}
};
private int completionIndex = 0;
private int lastCompletionLength = 0;
private String incomplete;
@ -1385,6 +1401,8 @@ public class ConversationFragment extends XmppFragment
new EditMessageActionModeCallback(this.binding.textinput));
}
binding.tabLayout.setListener(visibiltyChangeListener);
return binding.getRoot();
}
@ -1394,6 +1412,16 @@ public class ConversationFragment extends XmppFragment
Log.d(Config.LOGTAG, "ConversationFragment.onDestroyView()");
messageListAdapter.setOnContactPictureClicked(null);
messageListAdapter.setOnContactPictureLongClicked(null);
binding.conversationViewPager.setAdapter(null);
if (conversation != null) conversation.setupViewPager(null, null, null);
binding.tabLayout.setListener(null);
applyTabElevationFix(false);
}
private void applyTabElevationFix(boolean shouldElevate) {
View fragmentHostView = activity.getFragmentHostView();
if (fragmentHostView == null) return;
fragmentHostView. setElevation(shouldElevate ? activity.getResources().getDimension(R.dimen.toolbar_elevation) : 0);
}
private void quoteText(String text) {
@ -1699,12 +1727,33 @@ public class ConversationFragment extends XmppFragment
case R.id.action_toggle_pinned:
togglePinned();
break;
case R.id.action_refresh_feature_discovery:
refreshFeatureDiscovery();
default:
break;
}
return super.onOptionsItemSelected(item);
}
private void refreshFeatureDiscovery() {
Set<Map.Entry<String, Presence>> presences = conversation.getContact().getPresences().getPresencesMap().entrySet();
if (presences.isEmpty()) {
presences = new HashSet<>();
presences.add(new AbstractMap.SimpleEntry("", null));
}
for (Map.Entry<String, Presence> entry : presences) {
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();
refreshCommands(true);
});
});
}
}
private void startSearch() {
final Intent intent = new Intent(getActivity(), SearchActivity.class);
intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid());
@ -2615,6 +2664,8 @@ public class ConversationFragment extends XmppFragment
if (conversation == null) {
return false;
}
final Conversation originalConversation = this.conversation;
this.conversation = conversation;
// once we set the conversation all is good and it will automatically do the right thing in
// onStart()
@ -2687,9 +2738,65 @@ public class ConversationFragment extends XmppFragment
activity.xmppConnectionService
.getNotificationService()
.setOpenConversation(this.conversation);
if (commandAdapter != null && conversation != originalConversation) {
conversation.setupViewPager(binding.conversationViewPager, binding.tabLayout, originalConversation);
refreshCommands(false);
}
if (commandAdapter == null && conversation != null) {
conversation.setupViewPager(binding.conversationViewPager, binding.tabLayout, null);
commandAdapter = new CommandAdapter((XmppActivity) getActivity());
binding.commandsView.setAdapter(commandAdapter);
binding.commandsView.setOnItemClickListener((parent, view, position, id) -> {
if (activity == null) return;
final Element command = commandAdapter.getItem(position);
activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node"));
});
refreshCommands(false);
}
return true;
}
public void refreshForNewCaps() {
refreshCommands(true);
}
protected void refreshCommands(boolean delayShow) {
if (commandAdapter == null) return;
Jid commandJid = conversation.getContact().resourceWhichSupport(Namespace.COMMANDS);
if (commandJid == null && conversation.getJid().isDomainJid()) {
commandJid = conversation.getJid();
}
if (commandJid == null) {
conversation.hideViewPager();
} else {
if (!delayShow) 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();
} else if (delayShow) {
conversation.showViewPager();
}
});
});
}
}
private void resetUnreadMessagesCount() {
lastMessageUuid = null;
hideUnreadMessagesCount();
@ -2719,6 +2826,7 @@ public class ConversationFragment extends XmppFragment
final String downloadUuid = extras.getString(ConversationsActivity.EXTRA_DOWNLOAD_UUID);
final String text = extras.getString(Intent.EXTRA_TEXT);
final String nick = extras.getString(ConversationsActivity.EXTRA_NICK);
final String node = extras.getString(ConversationsActivity.EXTRA_NODE);
final String postInitAction =
extras.getString(ConversationsActivity.EXTRA_POST_INIT_ACTION);
final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
@ -2770,6 +2878,36 @@ public class ConversationFragment extends XmppFragment
attachFile(ATTACHMENT_CHOICE_RECORD_VOICE, false);
return;
}
if ("message".equals(postInitAction)) {
binding.conversationViewPager.post(() -> {
binding.conversationViewPager.setCurrentItem(0);
});
}
if ("command".equals(postInitAction)) {
binding.conversationViewPager.post(() -> {
PagerAdapter adapter = binding.conversationViewPager.getAdapter();
if (adapter != null && adapter.getCount() > 1) {
binding.conversationViewPager.setCurrentItem(1);
}
final String jid = extras.getString(ConversationsActivity.EXTRA_JID);
Jid commandJid = null;
if (jid != null) {
try {
commandJid = Jid.of(jid);
} catch (final IllegalArgumentException e) { }
}
if (commandJid == null || !commandJid.isFullJid()) {
final Jid discoJid = conversation.getContact().resourceWhichSupport(Namespace.COMMANDS);
if (discoJid != null) commandJid = discoJid;
}
if (node != null && commandJid != null) {
conversation.startCommand(commandFor(commandJid, node), activity.xmppConnectionService, activity);
}
});
return;
}
final Message message =
downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid);
if (message != null) {
@ -2777,6 +2915,23 @@ public class ConversationFragment extends XmppFragment
}
}
private Element commandFor(final Jid jid, final String node) {
if (commandAdapter != null) {
for (int i = 0; i < commandAdapter.getCount(); i++) {
Element command = commandAdapter.getItem(i);
final String commandNode = command.getAttribute("node");
if (commandNode == null || !commandNode.equals(node)) continue;
final Jid commandJid = command.getAttributeAsJid("jid");
if (commandJid != null && !commandJid.asBareJid().equals(jid.asBareJid())) continue;
return command;
}
}
return new Element("command", Namespace.COMMANDS).setAttribute("name", node).setAttribute("node", node).setAttribute("jid", jid);
}
private List<Uri> extractUris(final Bundle extras) {
final List<Uri> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
if (uris != null) {
@ -2998,6 +3153,7 @@ public class ConversationFragment extends XmppFragment
}
updateSendButton();
updateEditablity();
conversation.refreshSessions();
}
}
}

View file

@ -50,10 +50,12 @@ import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
@ -62,7 +64,9 @@ import androidx.databinding.DataBindingUtil;
import org.openintents.openpgp.util.OpenPgpApi;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config;
@ -85,10 +89,12 @@ import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import io.michaelrocks.libphonenumber.android.NumberParseException;
public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {
@ -102,6 +108,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
public static final String POST_ACTION_RECORD_VOICE = "record_voice";
public static final String EXTRA_TYPE = "type";
public static final String EXTRA_NODE = "node";
public static final String EXTRA_JID = "jid";
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
ACTION_VIEW_CONVERSATION,
@ -268,6 +276,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
final Fragment fragment = getFragmentManager().findFragmentById(id);
if (fragment instanceof XmppFragment) {
((XmppFragment) fragment).refresh();
// if (refreshForNewCaps) ((XmppFragment) fragment).refreshForNewCaps();
}
}
@ -420,6 +429,15 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
}
@Nullable
public View getFragmentHostView() {
if (binding.secondaryFragment != null) {
return binding.secondaryFragment;
} else {
return binding.mainFragment;
}
}
private void displayToast(final String msg) {
runOnUiThread(() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
}
@ -481,13 +499,50 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri);
if (conversation != null) {
openConversation(conversation, null);
if (xmppUri.isAction("command")) {
startCommand(conversation.getAccount(), xmppUri.getJid(), xmppUri.getParameter("node"));
} else {
Bundle extras = new Bundle();
extras.putString(Intent.EXTRA_TEXT, xmppUri.getBody());
if (xmppUri.isAction("message")) extras.putString(EXTRA_POST_INIT_ACTION, "message");
openConversation(conversation, extras);
}
return true;
}
}
return false;
}
public boolean onTelUriClicked(Uri uri, Account acct) {
final String tel;
try {
tel = PhoneNumberUtilWrapper.normalize(this, uri.getSchemeSpecificPart());
} catch (final IllegalArgumentException | NumberParseException | NullPointerException e) {
return false;
}
Set<String> gateways = new HashSet<>();
for (Account account : (acct == null ? xmppConnectionService.getAccounts() : List.of(acct))) {
for (Contact contact : account.getRoster().getContacts()) {
if (contact.getPresences().anyIdentity("gateway", "pstn") || contact.getPresences().anyIdentity("gateway", "sms")) {
if (acct == null) acct = account;
gateways.add(contact.getJid().asBareJid().toEscapedString());
}
}
}
for (String gateway : gateways) {
if (onXmppUriClicked(Uri.parse("xmpp:" + tel + "@" + gateway))) return true;
}
if (gateways.size() == 1 && acct != null) {
openConversation(xmppConnectionService.findOrCreateConversation(acct, Jid.ofLocalAndDomain(tel, gateways.iterator().next()), null, false, false, true, null), null);
return true;
}
return false;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (MenuDoubleTabUtil.shouldIgnoreTap()) {

View file

@ -546,6 +546,17 @@ public abstract class XmppActivity extends ActionBarActivity {
return getPreferences().getBoolean(name, getResources().getBoolean(res));
}
public void startCommand(final Account account, final Jid jid, final String node) {
Intent intent = new Intent(this, ConversationsActivity.class);
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, xmppConnectionService.findOrCreateConversation(account, jid, null, false, false, false, null).getUuid());
intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, "command");
intent.putExtra(ConversationsActivity.EXTRA_NODE, node);
intent.putExtra(ConversationsActivity.EXTRA_JID, (CharSequence) jid);
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
public void switchToConversation(Conversation conversation) {
switchToConversation(conversation, null);
}

View file

@ -38,6 +38,8 @@ public abstract class XmppFragment extends Fragment implements OnBackendConnecte
abstract void refresh();
public void refreshForNewCaps() { }
protected void runOnUiThread(Runnable runnable) {
final Activity activity = getActivity();
if (activity != null) {

View file

@ -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.databinding.CommandRowBinding;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.xml.Element;
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();
}
}

View file

@ -44,14 +44,21 @@ import android.widget.Toast;
import java.util.Arrays;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.ConversationsActivity;
@SuppressLint("ParcelCreator")
public class FixedURLSpan extends URLSpan {
private FixedURLSpan(String url) {
protected final Account account;
public FixedURLSpan(String url) {
this(url, null);
}
public FixedURLSpan(String url, Account account) {
super(url);
this.account = account;
}
public static void fix(final Editable editable) {
@ -74,6 +81,14 @@ public class FixedURLSpan extends URLSpan {
return;
}
}
if (("sms".equals(uri.getScheme()) || "tel".equals(uri.getScheme())) && context instanceof ConversationsActivity) {
if (((ConversationsActivity) context).onTelUriClicked(uri, account)) {
widget.playSoundEffect(0);
return;
}
}
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);

View file

@ -30,6 +30,9 @@
package eu.siacs.conversations.ui.util;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.SpannableStringBuilder;
@ -185,6 +188,33 @@ public class ShareUtil {
}
}
public static void copyLinkToClipboard(final Context context, final String url) {
final Uri uri = Uri.parse(url);
if ("xmpp".equals(uri.getScheme())) {
try {
final Jid jid = new XmppUri(uri).getJid();
if (copyTextToClipboard(context, jid.asBareJid().toString(), R.string.account_settings_jabber_id)) {
Toast.makeText(context, R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
} catch (final Exception e) { }
} else {
if (copyTextToClipboard(context, url, R.string.web_address)) {
Toast.makeText(context, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
}
public static boolean copyTextToClipboard(Context context, String text, int labelResId) {
ClipboardManager mClipBoardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
String label = context.getResources().getString(labelResId);
if (mClipBoardManager != null) {
ClipData mClipData = ClipData.newPlainText(label, text);
mClipBoardManager.setPrimaryClip(mClipData);
return true;
}
return false;
}
public static void copyLinkToClipboard(final XmppActivity activity, final Message message) {
final SpannableStringBuilder body = message.getBodyForDisplaying();
for (final String url : MyLinkify.extractLinks(body)) {

View file

@ -0,0 +1,37 @@
package eu.siacs.conversations.ui.widget;
import android.content.Context;
import android.util.AttributeSet;
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);
}
}

View file

@ -0,0 +1,40 @@
package eu.siacs.conversations.ui.widget;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class TabLayout extends com.google.android.material.tabs.TabLayout {
private VisibilityChangeListener listener = null;
public TabLayout(@NonNull Context context) {
super(context);
}
public TabLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TabLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setListener(VisibilityChangeListener listener) {
this.listener = listener;
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (listener != null) {
listener.onVisibilityChanged(visibility);
}
}
public interface VisibilityChangeListener {
void onVisibilityChanged(int visibility);
}
}

View file

@ -47,6 +47,9 @@ import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@ -148,7 +151,7 @@ public class XmppConnection implements Runnable {
private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
private final HashMap<String, Jid> commands = new HashMap<>();
private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks =
private final Hashtable<String, Pair<IqPacket, Pair<OnIqPacketReceived, ScheduledFuture>>> packetCallbacks =
new Hashtable<>();
private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
new HashSet<>();
@ -191,6 +194,7 @@ public class XmppConnection implements Runnable {
private String verifiedHostname = null;
private volatile Thread mThread;
private CountDownLatch mStreamCountDownLatch;
private static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account;
@ -1172,13 +1176,13 @@ public class XmppConnection implements Runnable {
} else {
OnIqPacketReceived callback = null;
synchronized (this.packetCallbacks) {
final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple =
final Pair<IqPacket, Pair<OnIqPacketReceived, ScheduledFuture>> packetCallbackDuple =
packetCallbacks.get(packet.getId());
if (packetCallbackDuple != null) {
// Packets to the server should have responses from the server
if (packetCallbackDuple.first.toServer(account)) {
if (packet.fromServer(account)) {
callback = packetCallbackDuple.second;
callback = packetCallbackDuple.second.first;
packetCallbacks.remove(packet.getId());
} else {
Log.e(
@ -1189,7 +1193,7 @@ public class XmppConnection implements Runnable {
} else {
if (packet.getFrom() != null
&& packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
callback = packetCallbackDuple.second;
callback = packetCallbackDuple.second.first;
packetCallbacks.remove(packet.getId());
} else {
Log.e(
@ -1833,11 +1837,13 @@ public class XmppConnection implements Runnable {
+ ": clearing "
+ this.packetCallbacks.size()
+ " iq callbacks");
final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator =
final Iterator<Pair<IqPacket, Pair<OnIqPacketReceived, ScheduledFuture>>> iterator =
this.packetCallbacks.values().iterator();
while (iterator.hasNext()) {
Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
callbacks.add(entry.second);
Pair<IqPacket, Pair<OnIqPacketReceived, ScheduledFuture>> entry = iterator.next();
if (entry.second.second == null || entry.second.second.cancel(false)) {
callbacks.add(entry.second.first);
}
iterator.remove();
}
}
@ -2250,18 +2256,36 @@ public class XmppConnection implements Runnable {
}
public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
return sendIqPacket(packet, callback, null);
}
public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback, Long timeout) {
packet.setFrom(account.getJid());
return this.sendUnmodifiedIqPacket(packet, callback, false);
return this.sendUnmodifiedIqPacket(packet, callback, false, timeout);
}
public synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
return sendUnmodifiedIqPacket(packet, callback, force, null);
}
public synchronized String sendUnmodifiedIqPacket(
final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
final IqPacket packet, final OnIqPacketReceived callback, boolean force, Long timeout) {
if (packet.getId() == null) {
packet.setAttribute("id", nextRandomId());
}
if (callback != null) {
synchronized (this.packetCallbacks) {
packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
ScheduledFuture timeoutFuture = null;
if (timeout != null) {
timeoutFuture = SCHEDULER.schedule(() -> {
synchronized (this.packetCallbacks) {
final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
final Pair<IqPacket, Pair<OnIqPacketReceived, ScheduledFuture>> removedCallback = packetCallbacks.remove(packet.getId());
if (removedCallback != null) removedCallback.second.first.onIqPacketReceived(account, failurePacket);
}
}, timeout, TimeUnit.SECONDS);
}
packetCallbacks.put(packet.getId(), new Pair<>(packet, new Pair<>(callback, timeoutFuture)));
}
}
this.sendPacket(packet, force);

View file

@ -75,4 +75,8 @@ public class Field extends Element {
public boolean isRequired() {
return hasChild("required");
}
public List<Option> getOptions() {
return Option.forField(this);
}
}

View file

@ -0,0 +1,66 @@
package eu.siacs.conversations.xmpp.forms;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
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;
protected final SVG icon;
public static List<Option> forField(Element field) {
List<Option> options = new ArrayList<>();
for (Element el : field.getChildren()) {
if (el.getNamespace() == null || !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"),
parseSVG(option.findChild("svg", "http://www.w3.org/2000/svg"))
);
}
public Option(final String value, final String label) {
this(value, label, null);
}
public Option(final String value, final String label, final SVG icon) {
this.value = value;
this.label = label == null ? value : label;
this.icon = icon;
}
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; }
public SVG getIcon() { return icon; }
private static SVG parseSVG(final Element svg) {
if (svg == null) return null;
try {
return SVG.getFromString(svg.toString());
} catch (final SVGParseException e) {
return null;
}
}
}

View file

@ -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>

View file

@ -40,6 +40,6 @@
<FrameLayout
android:id="@+id/main_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent" />
</LinearLayout>
</layout>

View file

@ -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">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textAllCaps="false" />
</layout>

View file

@ -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>

View file

@ -0,0 +1,67 @@
<?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:gravity="center"
android:textAppearance="@style/TextAppearance.Conversations.Subhead"
android:textColor="?attr/edit_text_color" />
<TextView
android:id="@+id/desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="8dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.Conversations.Status"
android:textColor="?android:textColorSecondary" />
<Button
android:id="@+id/default_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="40dp"
android:layout_marginLeft="40dp"
android:layout_marginBottom="24dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:textAllCaps="false"
android:minHeight="75dp" />
<eu.siacs.conversations.ui.widget.GridView
android:id="@+id/buttons"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"
android:paddingLeft="16dp"
android:horizontalSpacing="0dp"
android:verticalSpacing="0dp"
android:gravity="center"
android:numColumns="auto_fit" />
<Button
android:id="@+id/open_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="40dp"
android:gravity="center"
android:text="other / custom"
style="@style/Widget.Conversations.Button.Borderless" />
</LinearLayout>
</layout>

View file

@ -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>

View file

@ -0,0 +1,22 @@
<?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">
<androidx.cardview.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardCornerRadius="4dp">
<GridLayout
android:id="@+id/fields"
android:orientation="horizontal"
android:columnCount="2"
android:useDefaultMargins="true"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.cardview.widget.CardView>
</layout>

View file

@ -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>

View file

@ -0,0 +1,30 @@
<?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:background="?colorPrimary"
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>

View file

@ -0,0 +1,34 @@
<?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">
<ProgressBar
android:id="@+id/progressbar"
android:layout_width="match_parent"
android:layout_height="130dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="16dp" />
<TextView
android:id="@+id/text"
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:text="Please be patient..."
android:textColor="?attr/edit_text_color" />
</LinearLayout>
</layout>

View file

@ -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" />
<eu.siacs.conversations.ui.widget.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>

View file

@ -0,0 +1,16 @@
<?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:textIsSelectable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:textAppearance="@style/TextAppearance.Conversations.Body1"
android:textColor="?attr/edit_text_color" />
</layout>

View file

@ -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="8dp"
android:paddingRight="8dp"
android:paddingBottom="4dp"
android:textAppearance="@style/TextAppearance.Conversations.Caption" />
<ImageView
android:id="@+id/media_image"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="8dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:background="@color/black87"
android:longClickable="false"
android:scaleType="centerCrop"/>
<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_tertiary"
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>

View file

@ -0,0 +1,17 @@
<?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/command"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:paddingLeft="@dimen/avatar_item_distance"
android:paddingRight="@dimen/avatar_item_distance"
android:textAppearance="@style/TextAppearance.Conversations.Body1"
android:textColor="?attr/edit_text_color"
android:background="@drawable/list_choice" />
</layout>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,30 @@
<?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:suffixTextAppearance="@style/Widget.Conversations.EditText"
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>

View file

@ -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="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/desc"
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" />
<WebView
android:id="@+id/webview"
android:layout_below="@+id/desc"
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>

View file

@ -7,6 +7,30 @@
android:layout_height="match_parent"
android:background="?attr/color_background_secondary">
<eu.siacs.conversations.ui.widget.TabLayout
android:visibility="gone"
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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_secondary">
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView
android:id="@+id/messages_view"
android:layout_width="fill_parent"
@ -217,6 +241,34 @@
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>

View file

@ -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>

View file

@ -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" />

View file

@ -135,6 +135,12 @@
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="@string/refresh_feature_discovery"
app:showAsAction="never" />
</menu>
</item>

View file

@ -740,6 +740,7 @@
<string name="action_copy_location">Copy Location</string>
<string name="action_share_location">Share Location</string>
<string name="action_directions">Directions</string>
<string name="action_execute">Go</string>
<string name="title_activity_share_location">Share location</string>
<string name="title_activity_show_location">Show location</string>
<string name="share">Share</string>
@ -1053,4 +1054,5 @@
<string name="message_selection_title">%1$d selected</string>
<string name="contact_tag_general">General</string>
<string name="contact_tag_with_total">%1$s (%2$d)</string>
<string name="refresh_feature_discovery">Refresh Feature Discovery</string>
</resources>