From ceaa3135abbf254e92ebee1203a77a2481f7f0c5 Mon Sep 17 00:00:00 2001 From: Alex Palaistras Date: Sat, 8 Dec 2018 17:32:27 +0000 Subject: [PATCH 1/4] Checkout `xmpp-addr` library 0.8.0 (fa47cac8) locally Changes to this library will be implemented as additional commits. --- build.gradle | 2 +- libs/xmpp-addr/.gitignore | 1 + libs/xmpp-addr/build.gradle | 14 + .../java/rocks/xmpp/addr/AbstractJid.java | 179 +++++++ .../main/java/rocks/xmpp/addr/FullJid.java | 485 ++++++++++++++++++ .../src/main/java/rocks/xmpp/addr/Jid.java | 314 ++++++++++++ .../main/java/rocks/xmpp/addr/JidAdapter.java | 53 ++ .../java/rocks/xmpp/addr/MalformedJid.java | 130 +++++ .../java/rocks/xmpp/addr/package-info.java | 31 ++ .../rocks/xmpp/util/cache/DirectoryCache.java | 192 +++++++ .../java/rocks/xmpp/util/cache/LruCache.java | 228 ++++++++ .../rocks/xmpp/util/cache/package-info.java | 28 + settings.gradle | 1 + 13 files changed, 1657 insertions(+), 1 deletion(-) create mode 100644 libs/xmpp-addr/.gitignore create mode 100644 libs/xmpp-addr/build.gradle create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java create mode 100644 libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java diff --git a/build.gradle b/build.gradle index fc088490d..87ed2ac3f 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ dependencies { implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" implementation 'net.ypresto.androidtranscoder:android-transcoder:0.2.0' - implementation 'rocks.xmpp:xmpp-addr:0.8.0' + implementation project(':libs:xmpp-addr') implementation 'org.osmdroid:osmdroid-android:6.0.1' implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:1.3.0' diff --git a/libs/xmpp-addr/.gitignore b/libs/xmpp-addr/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/libs/xmpp-addr/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libs/xmpp-addr/build.gradle b/libs/xmpp-addr/build.gradle new file mode 100644 index 000000000..2d30752c4 --- /dev/null +++ b/libs/xmpp-addr/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'java-library' + +repositories { + google() + jcenter() + mavenCentral() +} + +dependencies { + implementation 'rocks.xmpp:precis:1.0.0' +} + +sourceCompatibility = "8" +targetCompatibility = "8" diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java new file mode 100644 index 000000000..5b286a717 --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java @@ -0,0 +1,179 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2017 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package rocks.xmpp.addr; + +import java.text.Collator; +import java.util.Objects; + +/** + * Abstract Jid implementation for both full and bare JIDs. + * + * @author Christian Schudt + */ +abstract class AbstractJid implements Jid { + + /** + * Checks if the JID is a full JID. + *
+ *

The term "full JID" refers to an XMPP address of the form <localpart@domainpart/resourcepart> (for a particular authorized client or device associated with an account) or of the form <domainpart/resourcepart> (for a particular resource or script associated with a server).

+ *
+ * + * @return True, if the JID is a full JID; otherwise false. + */ + @Override + public final boolean isFullJid() { + return getResource() != null; + } + + /** + * Checks if the JID is a bare JID. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @return True, if the JID is a bare JID; otherwise false. + */ + @Override + public final boolean isBareJid() { + return getResource() == null; + } + + @Override + public final boolean isDomainJid() { + return getLocal() == null; + } + + @Override + public final boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Jid)) { + return false; + } + Jid other = (Jid) o; + + return Objects.equals(getLocal(), other.getLocal()) + && Objects.equals(getDomain(), other.getDomain()) + && Objects.equals(getResource(), other.getResource()); + } + + @Override + public final int hashCode() { + return Objects.hash(getLocal(), getDomain(), getResource()); + } + + /** + * Compares this JID with another JID. First domain parts are compared. If these are equal, local parts are compared + * and if these are equal, too, resource parts are compared. + * + * @param o The other JID. + * @return The comparison result. + */ + @Override + public final int compareTo(Jid o) { + + if (this == o) { + return 0; + } + + if (o != null) { + final Collator collator = Collator.getInstance(); + int result; + // First compare domain parts. + if (getDomain() != null) { + result = o.getDomain() != null ? collator.compare(getDomain(), o.getDomain()) : -1; + } else { + result = o.getDomain() != null ? 1 : 0; + } + // If the domains are equal, compare local parts. + if (result == 0) { + if (getLocal() != null) { + // If this local part is not null, but the other is null, move this down (1). + result = o.getLocal() != null ? collator.compare(getLocal(), o.getLocal()) : 1; + } else { + // If this local part is null, but the other is not, move this up (-1). + result = o.getLocal() != null ? -1 : 0; + } + } + // If the local parts are equal, compare resource parts. + if (result == 0) { + if (getResource() != null) { + // If this resource part is not null, but the other is null, move this down (1). + return o.getResource() != null ? collator.compare(getResource(), o.getResource()) : 1; + } else { + // If this resource part is null, but the other is not, move this up (-1). + return o.getResource() != null ? -1 : 0; + } + } + return result; + } else { + return -1; + } + } + + @Override + public final int length() { + return toString().length(); + } + + @Override + public final char charAt(int index) { + return toString().charAt(index); + } + + @Override + public final CharSequence subSequence(int start, int end) { + return toString().subSequence(start, end); + } + + /** + * Returns the JID in its string representation, i.e. [ localpart "@" ] domainpart [ "/" resourcepart ]. + * + * @return The JID. + * @see #toEscapedString() + */ + @Override + public final String toString() { + return toString(getLocal(), getDomain(), getResource()); + } + + @Override + public final String toEscapedString() { + return toString(getEscapedLocal(), getDomain(), getResource()); + } + + static String toString(String local, String domain, String resource) { + StringBuilder sb = new StringBuilder(); + if (local != null) { + sb.append(local).append('@'); + } + sb.append(domain); + if (resource != null) { + sb.append('/').append(resource); + } + return sb.toString(); + } +} diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java new file mode 100644 index 000000000..2fcda3c42 --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java @@ -0,0 +1,485 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2017 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package rocks.xmpp.addr; + +import rocks.xmpp.precis.PrecisProfile; +import rocks.xmpp.precis.PrecisProfiles; +import rocks.xmpp.util.cache.LruCache; + +import java.net.IDN; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The implementation of the JID as described in Extensible Messaging and Presence Protocol (XMPP): Address Format. + *

+ * This class is thread-safe and immutable. + * + * @author Christian Schudt + * @see RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format + */ +final class FullJid extends AbstractJid { + + /** + * Escapes all disallowed characters and also backslash, when followed by a defined hex code for escaping. See 4. Business Rules. + */ + private static final Pattern ESCAPE_PATTERN = Pattern.compile("[ \"&'/:<>@]|\\\\(?=20|22|26|27|2f|3a|3c|3e|40|5c)"); + + private static final Pattern UNESCAPE_PATTERN = Pattern.compile("\\\\(20|22|26|27|2f|3a|3c|3e|40|5c)"); + + private static final Pattern JID = Pattern.compile("^((.*?)@)?([^/@]+)(/(.*))?$"); + + private static final IDNProfile IDN_PROFILE = new IDNProfile(); + + /** + * Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61 (halfwidth ideographic full stop). + */ + private static final String DOTS = "[.\u3002\uFF0E\uFF61]"; + + /** + * Label separators for domain labels, which should be mapped to "." (dot): IDEOGRAPHIC FULL STOP character (U+3002) + */ + private static final Pattern LABEL_SEPARATOR = Pattern.compile(DOTS); + + private static final Pattern LABEL_SEPARATOR_FINAL = Pattern.compile(DOTS + "$"); + + /** + * Caches the escaped JIDs. + */ + private static final Map ESCAPED_CACHE = new LruCache<>(5000); + + /** + * Caches the unescaped JIDs. + */ + private static final Map UNESCAPED_CACHE = new LruCache<>(5000); + + private static final long serialVersionUID = -3824234106101731424L; + + private final String escapedLocal; + + private final String local; + + private final String domain; + + private final String resource; + + private final Jid bareJid; + + /** + * Creates a full JID with local, domain and resource part. + * + * @param local The local part. + * @param domain The domain part. + * @param resource The resource part. + */ + FullJid(CharSequence local, CharSequence domain, CharSequence resource) { + this(local, domain, resource, false, null); + } + + private FullJid(final CharSequence local, final CharSequence domain, final CharSequence resource, final boolean doUnescape, Jid bareJid) { + final String enforcedLocalPart; + final String enforcedDomainPart; + final String enforcedResource; + + final String unescapedLocalPart; + + if (doUnescape) { + unescapedLocalPart = unescape(local); + } else { + unescapedLocalPart = local != null ? local.toString() : null; + } + + // Escape the local part, so that disallowed characters like the space characters pass the UsernameCaseMapped profile. + final String escapedLocalPart = escape(unescapedLocalPart); + + // If the domainpart includes a final character considered to be a label + // separator (dot) by [RFC1034], this character MUST be stripped from + // the domainpart before the JID of which it is a part is used for the + // purpose of routing an XML stanza, comparing against another JID, or + // constructing an XMPP URI or IRI [RFC5122]. In particular, such a + // character MUST be stripped before any other canonicalization steps + // are taken. + // Also validate, that the domain name can be converted to ASCII, i.e. validate the domain name (e.g. must not start with "_"). + final String strDomain = IDN.toASCII(LABEL_SEPARATOR_FINAL.matcher(Objects.requireNonNull(domain)).replaceAll(""), IDN.USE_STD3_ASCII_RULES); + enforcedLocalPart = escapedLocalPart != null ? PrecisProfiles.USERNAME_CASE_MAPPED.enforce(escapedLocalPart) : null; + enforcedResource = resource != null ? PrecisProfiles.OPAQUE_STRING.enforce(resource) : null; + // See https://tools.ietf.org/html/rfc5895#section-2 + enforcedDomainPart = IDN_PROFILE.enforce(strDomain); + + validateLength(enforcedLocalPart, "local"); + validateLength(enforcedResource, "resource"); + validateDomain(strDomain); + + this.local = unescape(enforcedLocalPart); + this.escapedLocal = enforcedLocalPart; + this.domain = enforcedDomainPart; + this.resource = enforcedResource; + if (bareJid != null) { + this.bareJid = bareJid; + } else { + this.bareJid = isBareJid() ? this : new AbstractJid() { + + @Override + public Jid asBareJid() { + return this; + } + + @Override + public Jid withLocal(CharSequence local) { + if (Objects.equals(local, this.getLocal())) { + return this; + } + return new FullJid(local, getDomain(), getResource(), false, null); + } + + @Override + public Jid withResource(CharSequence resource) { + if (Objects.equals(resource, this.getResource())) { + return this; + } + return new FullJid(getLocal(), getDomain(), resource, false, asBareJid()); + } + + @Override + public Jid atSubdomain(CharSequence subdomain) { + return new FullJid(getLocal(), Objects.requireNonNull(subdomain) + "." + getDomain(), getResource(), false, null); + } + + @Override + public String getLocal() { + return FullJid.this.getLocal(); + } + + @Override + public String getEscapedLocal() { + return FullJid.this.getEscapedLocal(); + } + + @Override + public String getDomain() { + return FullJid.this.getDomain(); + } + + @Override + public String getResource() { + return null; + } + }; + } + } + + /** + * Creates a JID from a string. The format must be + *

[ localpart "@" ] domainpart [ "/" resourcepart ]

+ * + * @param jid The JID. + * @param doUnescape If the jid parameter will be unescaped. + * @return The JID. + * @throws NullPointerException If the jid is null. + * @throws IllegalArgumentException If the jid could not be parsed or is not valid. + * @see XEP-0106: JID Escaping + */ + static Jid of(String jid, final boolean doUnescape) { + Objects.requireNonNull(jid, "jid must not be null."); + + jid = jid.trim(); + + if (jid.isEmpty()) { + throw new IllegalArgumentException("jid must not be empty."); + } + + Jid result; + if (doUnescape) { + result = UNESCAPED_CACHE.get(jid); + } else { + result = ESCAPED_CACHE.get(jid); + } + + if (result != null) { + return result; + } + + Matcher matcher = JID.matcher(jid); + if (matcher.matches()) { + Jid jidValue = new FullJid(matcher.group(2), matcher.group(3), matcher.group(5), doUnescape, null); + if (doUnescape) { + UNESCAPED_CACHE.put(jid, jidValue); + } else { + ESCAPED_CACHE.put(jid, jidValue); + } + return jidValue; + } else { + throw new IllegalArgumentException("Could not parse JID: " + jid); + } + } + + /** + * Escapes a local part. The characters {@code "&'/:<>@} (+ whitespace) are replaced with their respective escape characters. + * + * @param localPart The local part. + * @return The escaped local part or null. + * @see XEP-0106: JID Escaping + */ + private static String escape(final CharSequence localPart) { + if (localPart != null) { + final Matcher matcher = ESCAPE_PATTERN.matcher(localPart); + final StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(sb, "\\\\" + Integer.toHexString(matcher.group().charAt(0))); + } + matcher.appendTail(sb); + return sb.toString(); + } + return null; + } + + private static String unescape(final CharSequence localPart) { + if (localPart != null) { + final Matcher matcher = UNESCAPE_PATTERN.matcher(localPart); + final StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + final char c = (char) Integer.parseInt(matcher.group(1), 16); + if (c == '\\') { + matcher.appendReplacement(sb, "\\\\"); + } else { + matcher.appendReplacement(sb, String.valueOf(c)); + } + } + matcher.appendTail(sb); + return sb.toString(); + } + return null; + } + + private static void validateDomain(String domain) { + Objects.requireNonNull(domain, "domain must not be null."); + if (domain.contains("@")) { + // Prevent misuse of API. + throw new IllegalArgumentException("domain must not contain a '@' sign"); + } + validateLength(domain, "domain"); + } + + /** + * Validates that the length of a local, domain or resource part is not longer than 1023 characters. + * + * @param value The value. + * @param part The part, only used to produce an exception message. + */ + private static void validateLength(CharSequence value, CharSequence part) { + if (value != null) { + if (value.length() == 0) { + throw new IllegalArgumentException(part + " must not be empty."); + } + if (value.toString().getBytes(StandardCharsets.UTF_8).length > 1023) { + throw new IllegalArgumentException(part + " must not be greater than 1023 bytes."); + } + } + } + + /** + * Converts this JID into a bare JID, i.e. removes the resource part. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @return The bare JID. + * @see #withResource(CharSequence) + */ + @Override + public final Jid asBareJid() { + return bareJid; + } + + /** + * Gets the local part of the JID, also known as the name or node. + *
+ *

3.3. Localpart

+ *

The localpart of a JID is an optional identifier placed before the + * domainpart and separated from the latter by the '@' character. + * Typically, a localpart uniquely identifies the entity requesting and + * using network access provided by a server (i.e., a local account), + * although it can also represent other kinds of entities (e.g., a + * chatroom associated with a multi-user chat service [XEP-0045]). The + * entity represented by an XMPP localpart is addressed within the + * context of a specific domain (i.e., <localpart@domainpart>).

+ *
+ * + * @return The local part or null. + */ + @Override + public final String getLocal() { + return local; + } + + @Override + public final String getEscapedLocal() { + return escapedLocal; + } + + /** + * Gets the domain part. + *
+ *

3.2. Domainpart

+ *

The domainpart is the primary identifier and is the only REQUIRED + * element of a JID (a mere domainpart is a valid JID). Typically, + * a domainpart identifies the "home" server to which clients connect + * for XML routing and data management functionality.

+ *
+ * + * @return The domain part. + */ + @Override + public final String getDomain() { + return domain; + } + + /** + * Gets the resource part. + *
+ *

3.4. Resourcepart

+ *

The resourcepart of a JID is an optional identifier placed after the + * domainpart and separated from the latter by the '/' character. A + * resourcepart can modify either a <localpart@domainpart> address or a + * mere <domainpart> address. Typically, a resourcepart uniquely + * identifies a specific connection (e.g., a device or location) or + * object (e.g., an occupant in a multi-user chatroom [XEP-0045]) + * belonging to the entity associated with an XMPP localpart at a domain + * (i.e., <localpart@domainpart/resourcepart>).

+ *
+ * + * @return The resource part or null. + */ + @Override + public final String getResource() { + return resource; + } + + /** + * Creates a new JID with a new local part and the same domain and resource part of the current JID. + * + * @param local The local part. + * @return The JID with a new local part. + * @throws IllegalArgumentException If the local is not a valid local part. + * @see #withResource(CharSequence) + */ + @Override + public final Jid withLocal(CharSequence local) { + if (Objects.equals(local, this.getLocal())) { + return this; + } + return new FullJid(local, getDomain(), getResource(), false, null); + } + + /** + * Creates a new full JID with a resource and the same local and domain part of the current JID. + * + * @param resource The resource. + * @return The full JID with a resource. + * @throws IllegalArgumentException If the resource is not a valid resource part. + * @see #asBareJid() + * @see #withLocal(CharSequence) + */ + @Override + public final Jid withResource(CharSequence resource) { + if (Objects.equals(resource, this.getResource())) { + return this; + } + return new FullJid(getLocal(), getDomain(), resource, false, asBareJid()); + } + + /** + * Creates a new JID at a subdomain and at the same domain as this JID. + * + * @param subdomain The subdomain. + * @return The JID at a subdomain. + * @throws NullPointerException If subdomain is null. + * @throws IllegalArgumentException If subdomain is not a valid subdomain name. + */ + @Override + public final Jid atSubdomain(CharSequence subdomain) { + return new FullJid(getLocal(), Objects.requireNonNull(subdomain) + "." + getDomain(), getResource(), false, null); + } + + /** + * A profile for applying the rules for IDN as in RFC 5895. Although IDN doesn't use Precis, it's still very similar so that we can use the base class. + * + * @see RFC 5895 + */ + private static final class IDNProfile extends PrecisProfile { + + private IDNProfile() { + super(false); + } + + @Override + public String prepare(CharSequence input) { + return IDN.toUnicode(input.toString(), IDN.USE_STD3_ASCII_RULES); + } + + @Override + public String enforce(CharSequence input) { + // 4. Map IDEOGRAPHIC FULL STOP character (U+3002) to dot. + return applyAdditionalMappingRule( + // 3. All characters are mapped using Unicode Normalization Form C (NFC). + applyNormalizationRule( + // 2. Fullwidth and halfwidth characters (those defined with + // Decomposition Types and ) are mapped to their + // decomposition mappings + applyWidthMappingRule( + // 1. Uppercase characters are mapped to their lowercase equivalents + applyCaseMappingRule(prepare(input))))).toString(); + } + + @Override + protected CharSequence applyWidthMappingRule(CharSequence charSequence) { + return widthMap(charSequence); + } + + @Override + protected CharSequence applyAdditionalMappingRule(CharSequence charSequence) { + return LABEL_SEPARATOR.matcher(charSequence).replaceAll("."); + } + + @Override + protected CharSequence applyCaseMappingRule(CharSequence charSequence) { + return charSequence.toString().toLowerCase(); + } + + @Override + protected CharSequence applyNormalizationRule(CharSequence charSequence) { + return Normalizer.normalize(charSequence, Normalizer.Form.NFC); + } + + @Override + protected CharSequence applyDirectionalityRule(CharSequence charSequence) { + return charSequence; + } + } +} diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java new file mode 100644 index 000000000..34c33d93a --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java @@ -0,0 +1,314 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2017 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package rocks.xmpp.addr; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.io.Serializable; + +/** + * Represents the JID as described in Extensible Messaging and Presence Protocol (XMPP): Address Format. + *

+ * A JID consists of three parts: + *

+ * [ localpart "@" ] domainpart [ "/" resourcepart ] + *

+ * The easiest way to create a JID is to use the {@link #of(CharSequence)} method: + * ```java + * Jid jid = Jid.of("juliet@capulet.lit/balcony"); + * ``` + * You can then get the parts from it via the respective methods: + * ```java + * String local = jid.getLocal(); // juliet + * String domain = jid.getDomain(); // capulet.lit + * String resource = jid.getResource(); // balcony + * ``` + * Implementations of this interface should override equals() and hashCode(), so that different instances with the same value are equal: + * ```java + * Jid.of("romeo@capulet.lit/balcony").equals(Jid.of("romeo@capulet.lit/balcony")); // true + * ``` + * The default implementation of this class also supports XEP-0106: JID Escaping, i.e. + * ```java + * Jid.of("d'artagnan@musketeers.lit") + * ``` + * is escaped as d\\27artagnan@musketeers.lit. + *

+ * Implementations of this interface should be thread-safe and immutable. + * + * @author Christian Schudt + * @see RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format + */ +@XmlJavaTypeAdapter(JidAdapter.class) +public interface Jid extends Comparable, Serializable, CharSequence { + + /** + * The maximal length of a full JID, which is 3071. + *

+ *

3.1. Fundamentals

+ *

Each allowable portion of a JID (localpart, domainpart, and + * resourcepart) is 1 to 1023 octets in length, resulting in a maximum + * total size (including the '@' and '/' separators) of 3071 octets. + *

+ *
+ * Note that the length is based on bytes, not characters. + * + * @see #MAX_BARE_JID_LENGTH + */ + int MAX_FULL_JID_LENGTH = 3071; + + /** + * The maximal length of a bare JID, which is 2047 (1023 + 1 + 1023). + * Note that the length is based on bytes, not characters. + * + * @see #MAX_FULL_JID_LENGTH + */ + int MAX_BARE_JID_LENGTH = 2047; + + /** + * The service discovery feature used for determining support of JID escaping (jid\20escaping). + */ + String ESCAPING_FEATURE = "jid\\20escaping"; + + /** + * Returns a full JID with a domain and resource part, e.g. capulet.com/balcony + * + * @param local The local part. + * @param domain The domain. + * @param resource The resource part. + * @return The JID. + * @throws NullPointerException If the domain is null. + * @throws IllegalArgumentException If the domain, local or resource part are not valid. + */ + static Jid of(CharSequence local, CharSequence domain, CharSequence resource) { + return new FullJid(local, domain, resource); + } + + /** + * Creates a bare JID with only the domain part, e.g. capulet.com + * + * @param domain The domain. + * @return The JID. + * @throws NullPointerException If the domain is null. + * @throws IllegalArgumentException If the domain or local part are not valid. + */ + static Jid ofDomain(CharSequence domain) { + return new FullJid(null, domain, null); + } + + /** + * Creates a bare JID with a local and domain part, e.g. juliet@capulet.com + * + * @param local The local part. + * @param domain The domain. + * @return The JID. + * @throws NullPointerException If the domain is null. + * @throws IllegalArgumentException If the domain or local part are not valid. + */ + static Jid ofLocalAndDomain(CharSequence local, CharSequence domain) { + return new FullJid(local, domain, null); + } + + /** + * Creates a full JID with a domain and resource part, e.g. capulet.com/balcony + * + * @param domain The domain. + * @param resource The resource part. + * @return The JID. + * @throws NullPointerException If the domain is null. + * @throws IllegalArgumentException If the domain or resource are not valid. + */ + static Jid ofDomainAndResource(CharSequence domain, CharSequence resource) { + return new FullJid(null, domain, resource); + } + + /** + * Creates a JID from an unescaped string. The format must be + *

[ localpart "@" ] domainpart [ "/" resourcepart ]

+ * The input string will be escaped. + * + * @param jid The JID. + * @return The JID. + * @throws NullPointerException If the jid is null. + * @throws IllegalArgumentException If the jid could not be parsed or is not valid. + * @see XEP-0106: JID Escaping + */ + static Jid of(CharSequence jid) { + if (jid instanceof Jid) { + return (Jid) jid; + } + return FullJid.of(jid.toString(), false); + } + + /** + * Creates a JID from a escaped JID string. The format must be + *

[ localpart "@" ] domainpart [ "/" resourcepart ]

+ * This method should be used, when parsing JIDs from the XMPP stream. + * + * @param jid The JID. + * @return The JID. + * @throws NullPointerException If the jid is null. + * @throws IllegalArgumentException If the jid could not be parsed or is not valid. + * @see XEP-0106: JID Escaping + */ + static Jid ofEscaped(CharSequence jid) { + return FullJid.of(jid.toString(), true); + } + + /** + * Checks if the JID is a full JID. + *
+ *

The term "full JID" refers to an XMPP address of the form <localpart@domainpart/resourcepart> (for a particular authorized client or device associated with an account) or of the form <domainpart/resourcepart> (for a particular resource or script associated with a server).

+ *
+ * + * @return True, if the JID is a full JID; otherwise false. + */ + boolean isFullJid(); + + /** + * Checks if the JID is a bare JID. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @return True, if the JID is a bare JID; otherwise false. + */ + boolean isBareJid(); + + /** + * Checks if the JID is a domain JID, i.e. if it has no local part. + * + * @return True, if the JID is a domain JID, i.e. if it has no local part. + */ + boolean isDomainJid(); + + /** + * Gets the bare JID representation of this JID, i.e. removes the resource part. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @return The bare JID. + * @see #withResource(CharSequence) + */ + Jid asBareJid(); + + /** + * Creates a new JID with a new local part and the same domain and resource part of the current JID. + * + * @param local The local part. + * @return The JID with a new local part. + * @throws IllegalArgumentException If the local is not a valid local part. + * @see #withResource(CharSequence) + */ + Jid withLocal(CharSequence local); + + /** + * Creates a new full JID with a resource and the same local and domain part of the current JID. + * + * @param resource The resource. + * @return The full JID with a resource. + * @throws IllegalArgumentException If the resource is not a valid resource part. + * @see #asBareJid() + * @see #withLocal(CharSequence) + */ + Jid withResource(CharSequence resource); + + /** + * Creates a new JID at a subdomain and at the same domain as this JID. + * + * @param subdomain The subdomain. + * @return The JID at a subdomain. + * @throws NullPointerException If subdomain is null. + * @throws IllegalArgumentException If subdomain is not a valid subdomain name. + */ + Jid atSubdomain(CharSequence subdomain); + + /** + * Gets the local part of the JID, also known as the name or node. + *
+ *

3.3. Localpart

+ *

The localpart of a JID is an optional identifier placed before the + * domainpart and separated from the latter by the '@' character. + * Typically, a localpart uniquely identifies the entity requesting and + * using network access provided by a server (i.e., a local account), + * although it can also represent other kinds of entities (e.g., a + * chatroom associated with a multi-user chat service [XEP-0045]). The + * entity represented by an XMPP localpart is addressed within the + * context of a specific domain (i.e., <localpart@domainpart>).

+ *
+ * + * @return The local part or null. + * @see #getEscapedLocal() + */ + String getLocal(); + + /** + * Gets the escaped local part of the JID. + * + * @return The escaped local part or null. + * @see #getLocal() + * @since 0.8.0 + */ + String getEscapedLocal(); + + /** + * Gets the domain part. + *
+ *

3.2. Domainpart

+ *

The domainpart is the primary identifier and is the only REQUIRED + * element of a JID (a mere domainpart is a valid JID). Typically, + * a domainpart identifies the "home" server to which clients connect + * for XML routing and data management functionality.

+ *
+ * + * @return The domain part. + */ + String getDomain(); + + /** + * Gets the resource part. + *
+ *

3.4. Resourcepart

+ *

The resourcepart of a JID is an optional identifier placed after the + * domainpart and separated from the latter by the '/' character. A + * resourcepart can modify either a <localpart@domainpart> address or a + * mere <domainpart> address. Typically, a resourcepart uniquely + * identifies a specific connection (e.g., a device or location) or + * object (e.g., an occupant in a multi-user chatroom [XEP-0045]) + * belonging to the entity associated with an XMPP localpart at a domain + * (i.e., <localpart@domainpart/resourcepart>).

+ *
+ * + * @return The resource part or null. + */ + String getResource(); + + /** + * Returns the JID in escaped form as described in XEP-0106: JID Escaping. + * + * @return The escaped JID. + * @see #toString() + */ + String toEscapedString(); +} diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java new file mode 100644 index 000000000..dcd981e68 --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java @@ -0,0 +1,53 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package rocks.xmpp.addr; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +/** + * Converts a String representation of a JID to JID object and vice a versa. + */ +final class JidAdapter extends XmlAdapter { + + @Override + public Jid unmarshal(String v) { + if (v != null) { + try { + return Jid.ofEscaped(v); + } catch (Exception e) { + return MalformedJid.of(v, e); + } + } + return null; + } + + @Override + public String marshal(Jid v) { + if (v != null) { + return v.toEscapedString(); + } + return null; + } +} diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java new file mode 100644 index 000000000..ae99af7c0 --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java @@ -0,0 +1,130 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2017 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package rocks.xmpp.addr; + +import java.util.Objects; + +/** + * Represents a malformed JID in order to handle the jid-malformed error. + *

+ * This class is not intended to be publicly instantiable, but is used for malformed JIDs during parsing automatically. + * + * @author Christian Schudt + * @see RFC 6120, 8.3.3.8. jid-malformed + */ +public final class MalformedJid extends AbstractJid { + + private static final long serialVersionUID = -2896737611021417985L; + + private final String localPart; + + private final String domainPart; + + private final String resourcePart; + + private final Throwable cause; + + static MalformedJid of(final String jid, final Throwable cause) { + // Do some basic parsing without any further checks or validation. + final StringBuilder sb = new StringBuilder(jid); + // 1. Remove any portion from the first '/' character to the end of the + // string (if there is a '/' character present). + final int indexOfResourceDelimiter = jid.indexOf('/'); + final String resourcePart; + if (indexOfResourceDelimiter > -1) { + resourcePart = sb.substring(indexOfResourceDelimiter + 1); + sb.delete(indexOfResourceDelimiter, sb.length()); + } else { + resourcePart = null; + } + // 2. Remove any portion from the beginning of the string to the first + // '@' character (if there is an '@' character present). + final int indexOfAt = jid.indexOf('@'); + final String localPart; + if (indexOfAt > -1) { + localPart = sb.substring(0, indexOfAt); + sb.delete(0, indexOfAt + 1); + } else { + localPart = null; + } + return new MalformedJid(localPart, sb.toString(), resourcePart, cause); + } + + private MalformedJid(final String localPart, final String domainPart, final String resourcePart, final Throwable cause) { + this.localPart = localPart; + this.domainPart = domainPart; + this.resourcePart = resourcePart; + this.cause = cause; + } + + @Override + public final Jid asBareJid() { + return new MalformedJid(localPart, domainPart, null, cause); + } + + @Override + public Jid withLocal(CharSequence local) { + return new MalformedJid(local.toString(), domainPart, resourcePart, cause); + } + + @Override + public Jid withResource(CharSequence resource) { + return new MalformedJid(localPart, domainPart, resource.toString(), cause); + } + + @Override + public Jid atSubdomain(CharSequence subdomain) { + return new MalformedJid(localPart, Objects.requireNonNull(subdomain) + "." + domainPart, resourcePart, cause); + } + + @Override + public final String getLocal() { + return localPart; + } + + @Override + public final String getEscapedLocal() { + return localPart; + } + + @Override + public final String getDomain() { + return domainPart; + } + + @Override + public final String getResource() { + return resourcePart; + } + + /** + * Gets the cause why the JID is malformed. + * + * @return The cause. + */ + public final Throwable getCause() { + return cause; + } +} diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java new file mode 100644 index 000000000..e12485d5f --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java @@ -0,0 +1,31 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Provides classes for the XMPP Address Format (JID). + * + * @see Extensible Messaging and Presence Protocol (XMPP): Address Format + */ +package rocks.xmpp.addr; + diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java new file mode 100644 index 000000000..9b7d66d04 --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java @@ -0,0 +1,192 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package rocks.xmpp.util.cache; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A simple directory based cache for caching of persistent items like avatars or entity capabilities. + * + * @author Christian Schudt + */ +public final class DirectoryCache implements Map { + + private final Path cacheDirectory; + + public DirectoryCache(Path cacheDirectory) { + this.cacheDirectory = cacheDirectory; + } + + @Override + public final int size() { + try (final Stream files = cacheContent()) { + return (int) Math.min(files.count(), Integer.MAX_VALUE); + } + } + + @Override + public final boolean isEmpty() { + try (final Stream files = cacheContent()) { + return files.findAny().map(file -> Boolean.FALSE).orElse(Boolean.TRUE); + } + } + + @Override + public final boolean containsKey(Object key) { + return Files.exists(cacheDirectory.resolve(key.toString())); + } + + @Override + public final boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public final byte[] get(final Object key) { + return Optional.ofNullable(key).map(Object::toString).filter(((Predicate) String::isEmpty).negate()).map(cacheDirectory::resolve).filter(Files::isReadable).map(file -> { + try { + return Files.readAllBytes(file); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }).orElse(null); + } + + @Override + public final byte[] put(String key, byte[] value) { + // Make sure the directory exists. + byte[] data = get(key); + if (!Arrays.equals(data, value)) + try { + if (Files.notExists(cacheDirectory)) { + Files.createDirectories(cacheDirectory); + } + Path file = cacheDirectory.resolve(key); + Files.write(file, value); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return data; + } + + @Override + public final byte[] remove(Object key) { + byte[] data = get(key); + try { + Files.deleteIfExists(cacheDirectory.resolve(key.toString())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return data; + } + + @Override + public final void putAll(Map m) { + m.forEach(this::put); + } + + @Override + public final void clear() { + try { + Files.walkFileTree(cacheDirectory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.deleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + // Don't delete the cache directory itself. + if (!Files.isSameFile(dir, cacheDirectory)) { + Files.deleteIfExists(dir); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public final Set keySet() { + try (final Stream files = Files.list(cacheDirectory)) { + return Collections.unmodifiableSet(files.map(Path::getFileName).map(Path::toString).collect(Collectors.toSet())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public final Collection values() { + throw new UnsupportedOperationException(); + } + + @Override + public final Set> entrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public final void forEach(final BiConsumer action) { + if (Files.exists(cacheDirectory)) + try (final Stream files = cacheContent().filter(Files::isReadable)) { + files.forEach(file -> { + try { + action.accept(file.getFileName().toString(), Files.readAllBytes(file)); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } + + @SuppressWarnings("StreamResourceLeak") + private final Stream cacheContent() { + try { + return Files.walk(cacheDirectory).filter(Files::isRegularFile); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + +} diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java new file mode 100644 index 000000000..c2fbb0c3f --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java @@ -0,0 +1,228 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package rocks.xmpp.util.cache; + +import java.util.Collection; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * A simple concurrent implementation of a least-recently-used cache. + *

+ * This cache is keeps a maximal number of items in memory and removes the least-recently-used item, when new items are added. + * + * @param The key. + * @param The value. + * @author Christian Schudt + * @see http://javadecodedquestions.blogspot.de/2013/02/java-cache-static-data-loading.html + * @see http://stackoverflow.com/a/22891780 + */ +public final class LruCache implements Map { + private final int maxEntries; + + private final Map map; + + final Queue queue; + + public LruCache(final int maxEntries) { + this.maxEntries = maxEntries; + this.map = new ConcurrentHashMap<>(maxEntries); + // Don't use a ConcurrentLinkedQueue here. + // There's a JDK bug, leading to OutOfMemoryError and high CPU usage: + // https://bugs.openjdk.java.net/browse/JDK-8054446 + this.queue = new ConcurrentLinkedDeque<>(); + } + + @Override + public final int size() { + return map.size(); + } + + @Override + public final boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public final boolean containsKey(final Object key) { + return map.containsKey(key); + } + + @Override + public final boolean containsValue(final Object value) { + return map.containsValue(value); + } + + @SuppressWarnings("unchecked") + @Override + public final V get(final Object key) { + final V v = map.get(key); + if (v != null) { + // Remove the key from the queue and re-add it to the tail. It is now the most recently used key. + keyUsed((K) key); + } + return v; + } + + + @Override + public final V put(final K key, final V value) { + V v = map.put(key, value); + keyUsed(key); + limit(); + return v; + } + + @Override + public final V remove(final Object key) { + queue.remove(key); + return map.remove(key); + } + + + @Override + public final void putAll(final Map m) { + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public final void clear() { + queue.clear(); + map.clear(); + } + + @Override + public final Set keySet() { + return map.keySet(); + } + + @Override + public final Collection values() { + return map.values(); + } + + @Override + public final Set> entrySet() { + return map.entrySet(); + } + + + // Default methods + + @Override + public final V putIfAbsent(final K key, final V value) { + final V v = map.putIfAbsent(key, value); + if (v == null) { + keyUsed(key); + } + limit(); + return v; + } + + @Override + public final boolean remove(final Object key, final Object value) { + final boolean removed = map.remove(key, value); + if (removed) { + queue.remove(key); + } + return removed; + } + + @Override + public final boolean replace(final K key, final V oldValue, final V newValue) { + final boolean replaced = map.replace(key, oldValue, newValue); + if (replaced) { + keyUsed(key); + } + return replaced; + } + + @Override + public final V replace(final K key, final V value) { + final V v = map.replace(key, value); + if (v != null) { + keyUsed(key); + } + return v; + } + + @Override + public final V computeIfAbsent(final K key, final Function mappingFunction) { + return map.computeIfAbsent(key, mappingFunction.andThen(v -> { + keyUsed(key); + limit(); + return v; + })); + } + + @Override + public final V computeIfPresent(final K key, final BiFunction remappingFunction) { + return map.computeIfPresent(key, remappingFunction.andThen(v -> { + keyUsed(key); + limit(); + return v; + })); + } + + @Override + public final V compute(final K key, final BiFunction remappingFunction) { + return map.compute(key, remappingFunction.andThen(v -> { + keyUsed(key); + limit(); + return v; + })); + } + + @Override + public final V merge(K key, V value, BiFunction remappingFunction) { + return map.merge(key, value, remappingFunction.andThen(v -> { + keyUsed(key); + limit(); + return v; + })); + } + + private void limit() { + while (queue.size() > maxEntries) { + final K oldestKey = queue.poll(); + if (oldestKey != null) { + map.remove(oldestKey); + } + } + } + + private void keyUsed(final K key) { + // remove it from the queue and re-add it, to make it the most recently used key. + queue.remove(key); + queue.offer(key); + } +} \ No newline at end of file diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java new file mode 100644 index 000000000..c5e449d4c --- /dev/null +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java @@ -0,0 +1,28 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Provides simple cache implementations. + */ +package rocks.xmpp.util.cache; \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4193570fa..746b2c7e3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ +include ':libs:xmpp-addr' rootProject.name = 'Conversations' From 69ca58d0dba6c3ae547dfbd8ae38d464a09b23c6 Mon Sep 17 00:00:00 2001 From: Alex Palaistras Date: Sat, 8 Dec 2018 19:45:02 +0000 Subject: [PATCH 2/4] xmpp-addr: Backfill missing class method for Java 1.7 This backfills missing class methods for `java.nio.charset.StandardCharsets` and `java.util.Objects` for compatibility with platforms which do not support these (mainly Android SDK versions <= 18). --- .../java/rocks/xmpp/addr/AbstractJid.java | 10 ++--- .../main/java/rocks/xmpp/addr/FullJid.java | 37 +++++++++++++------ .../java/rocks/xmpp/addr/MalformedJid.java | 7 ++-- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java index 5b286a717..963c3a491 100644 --- a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java @@ -25,7 +25,7 @@ package rocks.xmpp.addr; import java.text.Collator; -import java.util.Objects; +import java.util.Arrays; /** * Abstract Jid implementation for both full and bare JIDs. @@ -75,14 +75,14 @@ abstract class AbstractJid implements Jid { } Jid other = (Jid) o; - return Objects.equals(getLocal(), other.getLocal()) - && Objects.equals(getDomain(), other.getDomain()) - && Objects.equals(getResource(), other.getResource()); + return (getLocal() == other.getLocal() || getLocal() != null && getLocal().equals(other.getLocal())) + && (getDomain() == other.getDomain() || getDomain() != null && getDomain().equals(other.getDomain())) + && (getResource() == other.getResource() || getResource() != null && getResource().equals(other.getResource())); } @Override public final int hashCode() { - return Objects.hash(getLocal(), getDomain(), getResource()); + return Arrays.hashCode(new String[]{getLocal(), getDomain(), getResource()}); } /** diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java index 2fcda3c42..24130fd1b 100644 --- a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java @@ -29,10 +29,9 @@ import rocks.xmpp.precis.PrecisProfiles; import rocks.xmpp.util.cache.LruCache; import java.net.IDN; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.text.Normalizer; import java.util.Map; -import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -109,6 +108,10 @@ final class FullJid extends AbstractJid { final String unescapedLocalPart; + if (domain == null) { + throw new NullPointerException(); + } + if (doUnescape) { unescapedLocalPart = unescape(local); } else { @@ -126,7 +129,7 @@ final class FullJid extends AbstractJid { // character MUST be stripped before any other canonicalization steps // are taken. // Also validate, that the domain name can be converted to ASCII, i.e. validate the domain name (e.g. must not start with "_"). - final String strDomain = IDN.toASCII(LABEL_SEPARATOR_FINAL.matcher(Objects.requireNonNull(domain)).replaceAll(""), IDN.USE_STD3_ASCII_RULES); + final String strDomain = IDN.toASCII(LABEL_SEPARATOR_FINAL.matcher(domain).replaceAll(""), IDN.USE_STD3_ASCII_RULES); enforcedLocalPart = escapedLocalPart != null ? PrecisProfiles.USERNAME_CASE_MAPPED.enforce(escapedLocalPart) : null; enforcedResource = resource != null ? PrecisProfiles.OPAQUE_STRING.enforce(resource) : null; // See https://tools.ietf.org/html/rfc5895#section-2 @@ -152,7 +155,7 @@ final class FullJid extends AbstractJid { @Override public Jid withLocal(CharSequence local) { - if (Objects.equals(local, this.getLocal())) { + if (local == this.getLocal() || local != null && local.equals(this.getLocal())) { return this; } return new FullJid(local, getDomain(), getResource(), false, null); @@ -160,7 +163,7 @@ final class FullJid extends AbstractJid { @Override public Jid withResource(CharSequence resource) { - if (Objects.equals(resource, this.getResource())) { + if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) { return this; } return new FullJid(getLocal(), getDomain(), resource, false, asBareJid()); @@ -168,7 +171,10 @@ final class FullJid extends AbstractJid { @Override public Jid atSubdomain(CharSequence subdomain) { - return new FullJid(getLocal(), Objects.requireNonNull(subdomain) + "." + getDomain(), getResource(), false, null); + if (subdomain == null) { + throw new NullPointerException(); + } + return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null); } @Override @@ -206,7 +212,9 @@ final class FullJid extends AbstractJid { * @see XEP-0106: JID Escaping */ static Jid of(String jid, final boolean doUnescape) { - Objects.requireNonNull(jid, "jid must not be null."); + if (jid == null) { + throw new NullPointerException("jid must not be null."); + } jid = jid.trim(); @@ -278,7 +286,9 @@ final class FullJid extends AbstractJid { } private static void validateDomain(String domain) { - Objects.requireNonNull(domain, "domain must not be null."); + if (domain == null) { + throw new NullPointerException("domain must not be null."); + } if (domain.contains("@")) { // Prevent misuse of API. throw new IllegalArgumentException("domain must not contain a '@' sign"); @@ -297,7 +307,7 @@ final class FullJid extends AbstractJid { if (value.length() == 0) { throw new IllegalArgumentException(part + " must not be empty."); } - if (value.toString().getBytes(StandardCharsets.UTF_8).length > 1023) { + if (value.toString().getBytes(Charset.forName("UTF-8")).length > 1023) { throw new IllegalArgumentException(part + " must not be greater than 1023 bytes."); } } @@ -391,7 +401,7 @@ final class FullJid extends AbstractJid { */ @Override public final Jid withLocal(CharSequence local) { - if (Objects.equals(local, this.getLocal())) { + if (local == this.getLocal() || local != null && local.equals(this.getLocal())) { return this; } return new FullJid(local, getDomain(), getResource(), false, null); @@ -408,7 +418,7 @@ final class FullJid extends AbstractJid { */ @Override public final Jid withResource(CharSequence resource) { - if (Objects.equals(resource, this.getResource())) { + if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) { return this; } return new FullJid(getLocal(), getDomain(), resource, false, asBareJid()); @@ -424,7 +434,10 @@ final class FullJid extends AbstractJid { */ @Override public final Jid atSubdomain(CharSequence subdomain) { - return new FullJid(getLocal(), Objects.requireNonNull(subdomain) + "." + getDomain(), getResource(), false, null); + if (subdomain != null) { + throw new NullPointerException(); + } + return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null); } /** diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java index ae99af7c0..f8605bfc1 100644 --- a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java +++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java @@ -24,8 +24,6 @@ package rocks.xmpp.addr; -import java.util.Objects; - /** * Represents a malformed JID in order to handle the jid-malformed error. *

@@ -96,7 +94,10 @@ public final class MalformedJid extends AbstractJid { @Override public Jid atSubdomain(CharSequence subdomain) { - return new MalformedJid(localPart, Objects.requireNonNull(subdomain) + "." + domainPart, resourcePart, cause); + if (subdomain == null) { + throw new NullPointerException(); + } + return new MalformedJid(localPart, subdomain + "." + domainPart, resourcePart, cause); } @Override From 08529041a51ff8fc8f5e415581da0b59200e05ce Mon Sep 17 00:00:00 2001 From: Alex Palaistras Date: Sat, 8 Dec 2018 19:50:13 +0000 Subject: [PATCH 3/4] Reduce `minSdkVersion` to 18, backfill missing methods This reduces the minimum SDK version to 18 (Android 4.3), which notably is the last supported version for the BlackBerry OS 10.3 Android compatibility layer. --- build.gradle | 2 +- .../conversations/ui/util/MyLinkify.java | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 87ed2ac3f..908fece82 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ android { compileSdkVersion 28 defaultConfig { - minSdkVersion 19 + minSdkVersion 18 targetSdkVersion 28 versionCode 307 versionName "2.3.9" diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index d350e51f9..d944f2d08 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -29,6 +29,7 @@ package eu.siacs.conversations.ui.util; +import android.os.Build; import android.text.Editable; import android.text.util.Linkify; @@ -80,7 +81,7 @@ public class MyLinkify { if (end < cs.length()) { // Reject strings that were probably matched only because they contain a dot followed by // by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java) - if (Character.isAlphabetic(cs.charAt(end-1)) && Character.isAlphabetic(cs.charAt(end))) { + if (isAlphabetic(cs.charAt(end-1)) && isAlphabetic(cs.charAt(end))) { return false; } } @@ -93,6 +94,24 @@ public class MyLinkify { return uri.isJidValid(); }; + private static boolean isAlphabetic(final int code) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return Character.isAlphabetic(code); + } + + switch (Character.getType(code)) { + case Character.UPPERCASE_LETTER: + case Character.LOWERCASE_LETTER: + case Character.TITLECASE_LETTER: + case Character.MODIFIER_LETTER: + case Character.OTHER_LETTER: + case Character.LETTER_NUMBER: + return true; + default: + return false; + } + } + public static void addLinks(Editable body, boolean includeGeo) { Linkify.addLinks(body, Patterns.XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null); Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER); From aaf5fa816b1809f2026fec5b82816f80b552b04c Mon Sep 17 00:00:00 2001 From: Alex Palaistras Date: Sun, 9 Dec 2018 21:32:42 +0000 Subject: [PATCH 4/4] Reduce `minSdkVersion` to 16, fix issues reported by lint This further reduces the minimum API level to 16, which should encompass most users stuck on older versions of Android (mainly BlackBerry OS and Jolla users). Several issues reported by code analysis were fixed, mainly around issues with layouts. --- build.gradle | 2 +- .../siacs/conversations/utils/Resolver.java | 4 ++- src/main/res/layout/activity_muc_details.xml | 36 +++++++++++++------ .../res/layout/activity_share_location.xml | 3 +- .../res/layout/activity_show_location.xml | 3 +- src/main/res/layout/fragment_conversation.xml | 33 ++++++++++++----- src/main/res/layout/media_preview.xml | 3 +- src/main/res/layout/message_sent.xml | 3 +- 8 files changed, 61 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 908fece82..2f30c289f 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ android { compileSdkVersion 28 defaultConfig { - minSdkVersion 18 + minSdkVersion 16 targetSdkVersion 28 versionCode 307 versionName "2.3.9" diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 680299a28..96ce63c90 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -65,7 +65,9 @@ public class Resolver { final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers"); useHardcodedDnsServers.setAccessible(true); useHardcodedDnsServers.setBoolean(dnsClient, false); - } catch (NoSuchFieldException | IllegalAccessException e) { + } catch (NoSuchFieldException e) { + Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); + } catch (IllegalAccessException e) { Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); } } diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index 7ab0894ce..1258c4db0 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -48,14 +48,17 @@ android:layout_height="@dimen/avatar_on_details_screen_size" android:layout_alignParentStart="true" app:riv_corner_radius="2dp" - android:layout_marginEnd="@dimen/avatar_item_distance"/> + android:layout_marginEnd="@dimen/avatar_item_distance" + android:layout_alignParentLeft="true" + android:layout_marginRight="@dimen/avatar_item_distance" /> + android:orientation="vertical" + android:layout_toRightOf="@+id/your_photo"> + android:orientation="vertical" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/edit_muc_name_button"> + android:visibility="gone" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/edit_muc_name_button"> + android:src="?attr/icon_edit_body" + android:layout_alignParentRight="true" /> @@ -151,7 +159,8 @@ android:layout_toStartOf="@+id/change_conference_button" android:text="@string/private_conference" android:textAppearance="@style/TextAppearance.Conversations.Body1" - /> + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/change_conference_button" /> + android:src="?attr/icon_settings" + android:layout_alignParentRight="true" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" + android:paddingLeft="4dp" /> @@ -302,7 +313,8 @@ android:alpha="?attr/icon_alpha" android:background="?attr/selectableItemBackgroundBorderless" android:padding="@dimen/image_button_padding" - android:src="?attr/icon_edit_body"/> + android:src="?attr/icon_edit_body" + android:layout_alignParentRight="true" /> + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/notification_status_button" /> + android:src="?attr/icon_notifications" + android:layout_alignParentRight="true" /> + android:src="?attr/icon_gps_fixed" + android:layout_alignParentRight="true" /> + app:tint="@color/white" + android:layout_alignParentRight="true" /> \ No newline at end of file diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 5a9048db7..9bd97c045 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -21,7 +21,8 @@ android:listSelector="@android:color/transparent" android:stackFromBottom="true" android:transcriptMode="normal" - tools:listitem="@layout/message_sent"> + tools:listitem="@layout/message_sent" + android:layout_alignParentLeft="true"> + app:useCompatPadding="true" + android:layout_alignParentRight="true" /> + app:backgroundColor="?attr/unread_count" + android:layout_alignRight="@+id/scroll_to_bottom_button" + tools:ignore="RtlCompat" + android:layout_marginRight="8dp" /> + android:background="?attr/color_background_primary" + android:layout_alignParentLeft="true"> + android:requiresFadingEdge="horizontal" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/textSendButton"> @@ -89,7 +97,9 @@ android:paddingBottom="12dp" android:paddingLeft="8dp" android:paddingRight="8dp" - android:paddingTop="12dp"> + android:paddingTop="12dp" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/textSendButton"> @@ -102,7 +112,8 @@ android:layout_centerVertical="true" android:background="?attr/color_background_primary" android:contentDescription="@string/send_message" - android:src="?attr/ic_send_text_offline"/> + android:src="?attr/ic_send_text_offline" + android:layout_alignParentRight="true" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark" + android:layout_alignParentLeft="true" + android:paddingLeft="24dp" + android:layout_toLeftOf="@+id/snackbar_action" /> + android:textStyle="bold" + android:layout_alignParentRight="true" /> diff --git a/src/main/res/layout/media_preview.xml b/src/main/res/layout/media_preview.xml index 07da56f4e..2262467aa 100644 --- a/src/main/res/layout/media_preview.xml +++ b/src/main/res/layout/media_preview.xml @@ -19,6 +19,7 @@ android:layout_alignParentTop="true" android:alpha="?attr/delete_icon_alpha" android:background="?attr/selectableItemBackgroundBorderless" - android:src="?attr/icon_cancel"/> + android:src="?attr/icon_cancel" + android:layout_alignParentRight="true" /> diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index d6a387c62..29ac6fc65 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/res/layout/message_sent.xml @@ -17,7 +17,8 @@ android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" android:layout_width="wrap_content" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:layout_alignParentRight="true">