add HttpUploadManager slot request
This commit is contained in:
parent
f9b3d42a8a
commit
f1fbf15fea
|
@ -8,6 +8,7 @@ import androidx.room.Query;
|
|||
import androidx.room.Transaction;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import im.conversations.android.database.entity.DiscoEntity;
|
||||
import im.conversations.android.database.entity.DiscoExtensionEntity;
|
||||
import im.conversations.android.database.entity.DiscoExtensionFieldEntity;
|
||||
|
@ -16,6 +17,7 @@ import im.conversations.android.database.entity.DiscoFeatureEntity;
|
|||
import im.conversations.android.database.entity.DiscoIdentityEntity;
|
||||
import im.conversations.android.database.entity.DiscoItemEntity;
|
||||
import im.conversations.android.database.model.Account;
|
||||
import im.conversations.android.database.model.DiscoItemWithExtension;
|
||||
import im.conversations.android.xmpp.Entity;
|
||||
import im.conversations.android.xmpp.EntityCapabilities;
|
||||
import im.conversations.android.xmpp.EntityCapabilities2;
|
||||
|
@ -24,7 +26,9 @@ import im.conversations.android.xmpp.model.data.Value;
|
|||
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
|
||||
import im.conversations.android.xmpp.model.disco.items.Item;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.jxmpp.jid.BareJid;
|
||||
import org.jxmpp.jid.DomainBareJid;
|
||||
import org.jxmpp.jid.Jid;
|
||||
import org.jxmpp.jid.parts.Resourcepart;
|
||||
|
||||
|
@ -223,4 +227,11 @@ public abstract class DiscoDao {
|
|||
"Discovering features for %s is not implemented",
|
||||
entity.getClass().getName()));
|
||||
}
|
||||
|
||||
@Query(
|
||||
"SELECT disco_item.discoId,address FROM disco_item JOIN disco_feature ON"
|
||||
+ " disco_item.discoId=disco_feature.discoId WHERE feature=:feature AND"
|
||||
+ " (address=:address OR parentAddress=:address)")
|
||||
public abstract ListenableFuture<List<DiscoItemWithExtension>> getItemByFeature(
|
||||
DomainBareJid address, final String feature);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package im.conversations.android.database.model;
|
||||
|
||||
import androidx.room.Relation;
|
||||
import com.google.common.collect.Iterables;
|
||||
import im.conversations.android.database.entity.DiscoExtensionFieldEntity;
|
||||
import im.conversations.android.database.entity.DiscoExtensionFieldValueEntity;
|
||||
import java.util.List;
|
||||
|
||||
public class DiscoExtension {
|
||||
|
||||
public long id;
|
||||
public String type;
|
||||
|
||||
@Relation(
|
||||
entity = DiscoExtensionFieldEntity.class,
|
||||
parentColumn = "id",
|
||||
entityColumn = "extensionId")
|
||||
public List<Field> fields;
|
||||
|
||||
public Field getField(final String name) {
|
||||
return Iterables.find(fields, f -> name.equals(f.field), null);
|
||||
}
|
||||
|
||||
public static class Field {
|
||||
|
||||
public long id;
|
||||
public String field;
|
||||
|
||||
@Relation(
|
||||
entity = DiscoExtensionFieldValueEntity.class,
|
||||
parentColumn = "id",
|
||||
entityColumn = "fieldId",
|
||||
projection = {"value"})
|
||||
public List<String> values;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package im.conversations.android.database.model;
|
||||
|
||||
import androidx.room.Relation;
|
||||
import com.google.common.collect.Iterables;
|
||||
import im.conversations.android.database.entity.DiscoExtensionEntity;
|
||||
import java.util.List;
|
||||
import org.jxmpp.jid.Jid;
|
||||
|
||||
public class DiscoItemWithExtension {
|
||||
|
||||
public Long discoId;
|
||||
public Jid address;
|
||||
|
||||
@Relation(
|
||||
entity = DiscoExtensionEntity.class,
|
||||
parentColumn = "discoId",
|
||||
entityColumn = "discoId")
|
||||
public List<DiscoExtension> extensions;
|
||||
|
||||
public DiscoExtension getExtension(final String type) {
|
||||
return Iterables.find(extensions, e -> type.equals(e.type), null);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import im.conversations.android.xmpp.model.pubsub.event.Retract;
|
|||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.jxmpp.jid.BareJid;
|
||||
import org.jxmpp.jid.Jid;
|
||||
import org.jxmpp.jid.impl.JidCreate;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -86,7 +87,7 @@ public class BookmarkManager extends AbstractManager {
|
|||
}
|
||||
}
|
||||
|
||||
public ListenableFuture<Void> publishBookmark(final Jid address, final boolean autoJoin) {
|
||||
public ListenableFuture<Void> publishBookmark(final BareJid address, final boolean autoJoin) {
|
||||
return publishBookmark(address, autoJoin, null);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.google.common.util.concurrent.ListenableFuture;
|
|||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import im.conversations.android.BuildConfig;
|
||||
import im.conversations.android.R;
|
||||
import im.conversations.android.database.model.DiscoItemWithExtension;
|
||||
import im.conversations.android.xml.Namespace;
|
||||
import im.conversations.android.xmpp.Entity;
|
||||
import im.conversations.android.xmpp.EntityCapabilities;
|
||||
|
@ -240,6 +241,13 @@ public class DiscoManager extends AbstractManager {
|
|||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
public ListenableFuture<List<DiscoItemWithExtension>> getServerItemByFeature(
|
||||
final String feature) {
|
||||
return getDatabase()
|
||||
.discoDao()
|
||||
.getItemByFeature(getAccount().address.asDomainBareJid(), feature);
|
||||
}
|
||||
|
||||
public boolean hasFeature(final Entity entity, final String feature) {
|
||||
return getDatabase().discoDao().hasFeature(getAccount().id, entity, feature);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,166 @@
|
|||
package im.conversations.android.xmpp.manager;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import im.conversations.android.database.model.DiscoItemWithExtension;
|
||||
import im.conversations.android.xml.Namespace;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||
import im.conversations.android.xmpp.model.upload.Get;
|
||||
import im.conversations.android.xmpp.model.upload.Header;
|
||||
import im.conversations.android.xmpp.model.upload.Put;
|
||||
import im.conversations.android.xmpp.model.upload.Request;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import org.jxmpp.jid.Jid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class HttpUploadManager extends AbstractManager {
|
||||
|
||||
private static final List<String> ALLOWED_HEADERS =
|
||||
Arrays.asList("Authorization", "Cookie", "Expires");
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(HttpUploadManager.class);
|
||||
|
||||
public HttpUploadManager(Context context, XmppConnection connection) {
|
||||
super(context, connection);
|
||||
}
|
||||
|
||||
public ListenableFuture<Slot> request(
|
||||
final String filename, final long size, final MediaType mediaType) {
|
||||
return Futures.transformAsync(
|
||||
getManager(DiscoManager.class).getServerItemByFeature(Namespace.HTTP_UPLOAD),
|
||||
items -> {
|
||||
final List<Service> services = Service.of(items);
|
||||
final Service service =
|
||||
Iterables.find(
|
||||
services,
|
||||
s -> s.maxFileSize == 0 || s.maxFileSize >= size,
|
||||
null);
|
||||
if (service == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"No upload service found that can handle files of size %d",
|
||||
size));
|
||||
}
|
||||
LOGGER.info("Requesting slot from {}", service.address);
|
||||
return request(service.address, filename, size, mediaType);
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private ListenableFuture<Slot> request(
|
||||
final Jid address, final String filename, final long size, final MediaType mediaType) {
|
||||
final var iq = new Iq(Iq.Type.GET);
|
||||
iq.setTo(address);
|
||||
final var request = iq.addExtension(new Request());
|
||||
request.setFilename(filename);
|
||||
request.setSize(size);
|
||||
request.setContentType(mediaType.type());
|
||||
final var iqFuture = connection.sendIqPacket(iq);
|
||||
// catch and rethrow 'file-too-large'
|
||||
return Futures.transform(
|
||||
iqFuture,
|
||||
result -> {
|
||||
final var slot =
|
||||
result.getExtension(
|
||||
im.conversations.android.xmpp.model.upload.Slot.class);
|
||||
if (slot == null) {
|
||||
throw new IllegalStateException("No slot in response");
|
||||
}
|
||||
final var get = slot.getExtension(Get.class);
|
||||
final var put = slot.getExtension(Put.class);
|
||||
final var getUrl = get == null ? null : get.getUrl();
|
||||
final var putUrl = put == null ? null : put.getUrl();
|
||||
if (get == null || put == null) {
|
||||
throw new IllegalStateException("Missing put or get URL in response");
|
||||
}
|
||||
final ImmutableMap.Builder<String, String> headers =
|
||||
new ImmutableMap.Builder<>();
|
||||
for (final Header header : put.getHeaders()) {
|
||||
final String name = header.getHeaderName();
|
||||
final String value = header.getContent();
|
||||
if (value != null && ALLOWED_HEADERS.contains(name)) {
|
||||
headers.put(name, value);
|
||||
}
|
||||
}
|
||||
return new Slot(putUrl, headers.buildKeepingLast(), getUrl);
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
public static class Slot {
|
||||
public final HttpUrl put;
|
||||
public final Map<String, String> headers;
|
||||
public final HttpUrl get;
|
||||
|
||||
public Slot(HttpUrl put, final Map<String, String> headers, final HttpUrl get) {
|
||||
this.put = put;
|
||||
this.headers = headers;
|
||||
this.get = get;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("put", put)
|
||||
.add("headers", headers)
|
||||
.add("get", get)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static class Service {
|
||||
public final Jid address;
|
||||
public final long maxFileSize;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("address", address)
|
||||
.add("maxFileSize", maxFileSize)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private Service(Jid address, long maxFileSize) {
|
||||
this.address = address;
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
public static List<Service> of(List<DiscoItemWithExtension> items) {
|
||||
return Lists.transform(items, Service::of);
|
||||
}
|
||||
|
||||
private static Service of(final DiscoItemWithExtension item) {
|
||||
final var discoExtension = item.getExtension(Namespace.HTTP_UPLOAD);
|
||||
final long maxFileSize;
|
||||
if (discoExtension == null) {
|
||||
maxFileSize = 0;
|
||||
} else {
|
||||
final var field = discoExtension.getField("max-file-size");
|
||||
final var value = field == null ? null : Iterables.getFirst(field.values, null);
|
||||
if (Strings.isNullOrEmpty(value)) {
|
||||
maxFileSize = 0;
|
||||
} else {
|
||||
maxFileSize = Longs.tryParse(value);
|
||||
}
|
||||
}
|
||||
return new Service(item.address, maxFileSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,8 @@ public class MultiUserChatManager extends AbstractManager {
|
|||
}
|
||||
|
||||
private void enterExisting(final MucWithNick mucWithNick, final InfoQuery infoQuery) {
|
||||
if (infoQuery.hasFeature(Namespace.MUC)) {
|
||||
if (infoQuery.hasFeature(Namespace.MUC)
|
||||
&& infoQuery.hasIdentityWithCategory("conference")) {
|
||||
sendJoinPresence(mucWithNick);
|
||||
} else {
|
||||
getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC);
|
||||
|
@ -91,8 +92,6 @@ public class MultiUserChatManager extends AbstractManager {
|
|||
final MucUser mucUser = presencePacket.getExtension(MucUser.class);
|
||||
Preconditions.checkArgument(
|
||||
mucUser.getStatus().contains(MucUser.STATUS_CODE_SELF_PRESENCE));
|
||||
// TODO flag chat as joined
|
||||
LOGGER.info("Received self presence for {}", presencePacket.getFrom());
|
||||
final var database = getDatabase();
|
||||
database.runInTransaction(
|
||||
() -> {
|
||||
|
@ -107,7 +106,6 @@ public class MultiUserChatManager extends AbstractManager {
|
|||
"Available presence received for archived or non existent chat");
|
||||
return;
|
||||
}
|
||||
// TODO set status codes
|
||||
database.chatDao()
|
||||
.setMucState(
|
||||
chatIdentifier.id, MucState.AVAILABLE, mucUser.getStatus());
|
||||
|
@ -132,8 +130,11 @@ public class MultiUserChatManager extends AbstractManager {
|
|||
} else if (chatIdentifier.archived) {
|
||||
database.chatDao().setMucState(chatIdentifier.id, null);
|
||||
} else {
|
||||
// TODO set status codes
|
||||
database.chatDao().setMucState(chatIdentifier.id, MucState.UNAVAILABLE);
|
||||
database.chatDao()
|
||||
.setMucState(
|
||||
chatIdentifier.id,
|
||||
MucState.UNAVAILABLE,
|
||||
mucUser.getStatus());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -31,4 +31,8 @@ public class InfoQuery extends Extension {
|
|||
public Collection<Identity> getIdentities() {
|
||||
return this.getExtensions(Identity.class);
|
||||
}
|
||||
|
||||
public boolean hasIdentityWithCategory(final String category) {
|
||||
return Iterables.any(getIdentities(), i -> category.equals(i.getCategory()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package im.conversations.android.xmpp.model.upload;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
@XmlElement
|
||||
public class Get extends Extension {
|
||||
|
||||
public Get() {
|
||||
super(Get.class);
|
||||
}
|
||||
|
||||
public HttpUrl getUrl() {
|
||||
final var url = this.getAttribute("url");
|
||||
if (Strings.isNullOrEmpty(url)) {
|
||||
return null;
|
||||
}
|
||||
return HttpUrl.parse(url);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package im.conversations.android.xmpp.model.upload;
|
||||
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
|
||||
@XmlElement
|
||||
public class Header extends Extension {
|
||||
|
||||
public Header() {
|
||||
super(Header.class);
|
||||
}
|
||||
|
||||
public String getHeaderName() {
|
||||
return this.getAttribute("name");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package im.conversations.android.xmpp.model.upload;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
import java.util.Collection;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
@XmlElement
|
||||
public class Put extends Extension {
|
||||
|
||||
public Put() {
|
||||
super(Put.class);
|
||||
}
|
||||
|
||||
public HttpUrl getUrl() {
|
||||
final var url = this.getAttribute("url");
|
||||
if (Strings.isNullOrEmpty(url)) {
|
||||
return null;
|
||||
}
|
||||
return HttpUrl.parse(url);
|
||||
}
|
||||
|
||||
public Collection<Header> getHeaders() {
|
||||
return this.getExtensions(Header.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package im.conversations.android.xmpp.model.upload;
|
||||
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
|
||||
@XmlElement
|
||||
public class Request extends Extension {
|
||||
|
||||
public Request() {
|
||||
super(Request.class);
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.setAttribute("filename", filename);
|
||||
}
|
||||
|
||||
public void setSize(long size) {
|
||||
this.setAttribute("size", size);
|
||||
}
|
||||
|
||||
public void setContentType(String type) {
|
||||
this.setAttribute("content-ype", type);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package im.conversations.android.xmpp.model.upload;
|
||||
|
||||
import im.conversations.android.annotation.XmlElement;
|
||||
import im.conversations.android.xmpp.model.Extension;
|
||||
|
||||
@XmlElement
|
||||
public class Slot extends Extension {
|
||||
|
||||
public Slot() {
|
||||
super(Slot.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@XmlPackage(namespace = Namespace.HTTP_UPLOAD)
|
||||
package im.conversations.android.xmpp.model.upload;
|
||||
|
||||
import im.conversations.android.annotation.XmlPackage;
|
||||
import im.conversations.android.xml.Namespace;
|
Loading…
Reference in a new issue