diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 73f4c7553..78bc56328 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -277,6 +277,11 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
+
+
+
diff --git a/jni/dc_wrapper.c b/jni/dc_wrapper.c
index 7129ec1e3..90563b8c1 100644
--- a/jni/dc_wrapper.c
+++ b/jni/dc_wrapper.c
@@ -86,6 +86,19 @@ static jstring jstring_new__(JNIEnv* env, const char* a)
#define CTIMESTAMP(a) (((jlong)a)/((jlong)1000))
+static jbyteArray ptr2jbyteArray(JNIEnv *env, const void* ptr, size_t len) {
+ if (ptr == NULL || len <= 0) {
+ return NULL;
+ }
+ jbyteArray ret = (*env)->NewByteArray(env, len);
+ if (ret == NULL) {
+ return NULL;
+ }
+ (*env)->SetByteArrayRegion(env, ret, 0, len, (const jbyte*)ptr);
+ return ret;
+}
+
+
static jintArray dc_array2jintArray_n_unref(JNIEnv *env, dc_array_t* ca)
{
/* takes a C-array of type dc_array_t and converts it it a Java-Array.
@@ -704,6 +717,26 @@ JNIEXPORT jint Java_com_b44t_messenger_DcContext_sendVideochatInvitation(JNIEnv
}
+JNIEXPORT jboolean Java_com_b44t_messenger_DcContext_sendWebxdcStatusUpdate(JNIEnv *env, jobject obj, jint msg_id, jstring payload, jstring descr)
+{
+ CHAR_REF(payload);
+ CHAR_REF(descr);
+ jboolean ret = dc_send_webxdc_status_update(get_dc_context(env, obj), msg_id, payloadPtr, descrPtr) != 0;
+ CHAR_UNREF(descr);
+ CHAR_UNREF(payload);
+ return ret;
+}
+
+
+JNIEXPORT jstring Java_com_b44t_messenger_DcContext_getWebxdcStatusUpdates(JNIEnv *env, jobject obj, jint msg_id, jint status_update_id)
+{
+ char* temp = dc_get_webxdc_status_updates(get_dc_context(env, obj), msg_id, status_update_id);
+ jstring ret = JSTRING_NEW(temp);
+ dc_str_unref(temp);
+ return ret;
+}
+
+
JNIEXPORT jint Java_com_b44t_messenger_DcContext_addDeviceMsg(JNIEnv *env, jobject obj, jstring label, jobject msg)
{
CHAR_REF(label);
@@ -1528,6 +1561,28 @@ JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getFilename(JNIEnv *env, jobject
}
+JNIEXPORT jbyteArray Java_com_b44t_messenger_DcMsg_getWebxdcBlob(JNIEnv *env, jobject obj, jstring filename)
+{
+ jbyteArray ret = NULL;
+ CHAR_REF(filename)
+ size_t ptrSize = 0;
+ char* ptr = dc_msg_get_webxdc_blob(get_dc_msg(env, obj), filenamePtr, &ptrSize);
+ ret = ptr2jbyteArray(env, ptr, ptrSize);
+ dc_str_unref(ptr);
+ CHAR_UNREF(filename)
+ return ret;
+}
+
+
+JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getWebxdcInfoJson(JNIEnv *env, jobject obj)
+{
+ char* temp = dc_msg_get_webxdc_info(get_dc_msg(env, obj));
+ jstring ret = JSTRING_NEW(temp);
+ dc_str_unref(temp);
+ return ret;
+}
+
+
JNIEXPORT jboolean Java_com_b44t_messenger_DcMsg_isForwarded(JNIEnv *env, jobject obj)
{
return dc_msg_is_forwarded(get_dc_msg(env, obj))!=0;
diff --git a/res/layout/conversation_activity_attachment_editor_stub.xml b/res/layout/conversation_activity_attachment_editor_stub.xml
index 16b868210..40b60e5f6 100644
--- a/res/layout/conversation_activity_attachment_editor_stub.xml
+++ b/res/layout/conversation_activity_attachment_editor_stub.xml
@@ -58,6 +58,17 @@
android:padding="8dp"
android:background="@drawable/message_bubble_background_sent_alone"/>
+
+
diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml
index b645ef64e..0cf64875f 100644
--- a/res/layout/conversation_item_received.xml
+++ b/res/layout/conversation_item_received.xml
@@ -143,6 +143,16 @@
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
+
+
+
+
+
diff --git a/res/layout/webxdc_view.xml b/res/layout/webxdc_view.xml
new file mode 100644
index 000000000..d7191df7e
--- /dev/null
+++ b/res/layout/webxdc_view.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/raw/webxdc.js b/res/raw/webxdc.js
new file mode 100644
index 000000000..249d5b0dc
--- /dev/null
+++ b/res/raw/webxdc.js
@@ -0,0 +1,26 @@
+window.webxdc = (() => {
+ var update_listener = () => {};
+
+ window.__webxdcUpdate = (updateId) => {
+ var updates = JSON.parse(InternalJSApi.getStatusUpdates(updateId));
+ if (updates.length === 1) {
+ update_listener(updates[0]);
+ }
+ };
+
+ return {
+ selfAddr: () => InternalJSApi.selfAddr(),
+
+ selfName: () => InternalJSApi.selfName(),
+
+ setUpdateListener: (cb) => (update_listener = cb),
+
+ getAllUpdates: () => {
+ return JSON.parse(InternalJSApi.getStatusUpdates(0));
+ },
+
+ sendUpdate: (payload, descr) => {
+ InternalJSApi.sendStatusUpdate(JSON.stringify(payload), descr);
+ },
+ };
+})();
diff --git a/src/com/b44t/messenger/DcContext.java b/src/com/b44t/messenger/DcContext.java
index a74b2fb31..25303c190 100644
--- a/src/com/b44t/messenger/DcContext.java
+++ b/src/com/b44t/messenger/DcContext.java
@@ -27,6 +27,7 @@ public class DcContext {
public final static int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061;
public final static int DC_EVENT_CONNECTIVITY_CHANGED = 2100;
public final static int DC_EVENT_SELFAVATAR_CHANGED = 2110;
+ public final static int DC_EVENT_WEBXDC_STATUS_UPDATE = 2120;
public final static int DC_IMEX_EXPORT_SELF_KEYS = 1;
public final static int DC_IMEX_IMPORT_SELF_KEYS = 2;
@@ -195,6 +196,8 @@ public class DcContext {
public native int sendMsg (int chat_id, DcMsg msg);
public native int sendTextMsg (int chat_id, String text);
public native int sendVideochatInvitation(int chat_id);
+ public native boolean sendWebxdcStatusUpdate(int msg_id, String payload, String descr);
+ public native String getWebxdcStatusUpdates(int msg_id, int status_update_id);
public native int addDeviceMsg (String label, DcMsg msg);
public native boolean wasDeviceMsgEverAdded(String label);
public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); }
diff --git a/src/com/b44t/messenger/DcMsg.java b/src/com/b44t/messenger/DcMsg.java
index 54e853270..fbf12605d 100644
--- a/src/com/b44t/messenger/DcMsg.java
+++ b/src/com/b44t/messenger/DcMsg.java
@@ -1,5 +1,8 @@
package com.b44t.messenger;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.io.File;
import java.util.Set;
@@ -15,12 +18,14 @@ public class DcMsg {
public final static int DC_MSG_VIDEO = 50;
public final static int DC_MSG_FILE = 60;
public final static int DC_MSG_VIDEOCHAT_INVITATION = 70;
+ public final static int DC_MSG_WEBXDC = 80;
public final static int DC_STATE_UNDEFINED = 0;
public final static int DC_STATE_IN_FRESH = 10;
public final static int DC_STATE_IN_NOTICED = 13;
public final static int DC_STATE_IN_SEEN = 16;
public final static int DC_STATE_OUT_PREPARING = 18;
+ public final static int DC_STATE_OUT_DRAFT = 19;
public final static int DC_STATE_OUT_PENDING = 20;
public final static int DC_STATE_OUT_ERROR = 24;
public final static int DC_STATE_OUT_DELIVERED = 26;
@@ -110,6 +115,15 @@ public class DcMsg {
public native String getFilemime ();
public native String getFilename ();
public native long getFilebytes ();
+ public native byte[] getWebxdcBlob (String filename);
+ public JSONObject getWebxdcInfo () {
+ try {
+ return new JSONObject(getWebxdcInfoJson());
+ } catch(Exception e) {
+ e.printStackTrace();
+ return new JSONObject();
+ }
+ }
public native boolean isForwarded ();
public native boolean isInfo ();
public native boolean isSetupMessage ();
@@ -206,4 +220,5 @@ public class DcMsg {
private native long getSummaryCPtr (long chatCPtr);
private native void setQuoteCPtr (long quoteCPtr);
private native long getQuotedMsgCPtr ();
+ private native String getWebxdcInfoJson ();
};
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index 9443e0b28..65ab38397 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -46,6 +46,7 @@ import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
+import android.webkit.MimeTypeMap;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -836,6 +837,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case DcMsg.DC_MSG_VIDEO:
setMedia(uri, MediaType.VIDEO).addListener(listener);
break;
+ case DcMsg.DC_MSG_WEBXDC:
+ setMedia(draft, MediaType.DOCUMENT).addListener(listener);
+ break;
default:
setMedia(uri, MediaType.DOCUMENT).addListener(listener);
break;
@@ -1012,7 +1016,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(false);
}
- return attachmentManager.setMedia(glideRequests, uri, mediaType, 0, 0);
+ return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
+ }
+
+ private ListenableFuture setMedia(DcMsg msg, @NonNull MediaType mediaType) {
+ return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId);
}
private void addAttachmentContactInfo(Intent data) {
@@ -1029,7 +1037,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return dcChat.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED;
}
- private String getRealPathFromAttachment(Attachment attachment) {
+ public static String getRealPathFromAttachment(Context context, Attachment attachment) {
try {
// get file in the blobdir as `/[-].`
String filename = attachment.getFileName();
@@ -1045,11 +1053,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
filename = filename.substring(0, i);
}
}
- String path = DcHelper.getBlobdirFile(dcContext, filename, ext);
+ String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext);
// copy content to this file
if(path!=null) {
- InputStream inputStream = PartAuthority.getAttachmentStream(this, attachment.getDataUri());
+ InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream);
}
@@ -1095,29 +1103,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
try {
- List attachments = slideDeck.asAttachments();
- for (Attachment attachment : attachments) {
- String contentType = attachment.getContentType();
- if (MediaUtil.isImageType(contentType) && slideDeck.getDocumentSlide()==null) {
- msg = new DcMsg(dcContext,
- MediaUtil.isGif(contentType) ? DcMsg.DC_MSG_GIF : DcMsg.DC_MSG_IMAGE);
- msg.setDimension(attachment.getWidth(), attachment.getHeight());
+ if (slideDeck.getWebxdctDraftId() != 0) {
+ msg = dcContext.getDraft(chatId);
+ } else {
+ List attachments = slideDeck.asAttachments();
+ for (Attachment attachment : attachments) {
+ String contentType = attachment.getContentType();
+ if (MediaUtil.isImageType(contentType) && slideDeck.getDocumentSlide() == null) {
+ msg = new DcMsg(dcContext,
+ MediaUtil.isGif(contentType) ? DcMsg.DC_MSG_GIF : DcMsg.DC_MSG_IMAGE);
+ msg.setDimension(attachment.getWidth(), attachment.getHeight());
+ } else if (MediaUtil.isAudioType(contentType)) {
+ msg = new DcMsg(dcContext,
+ attachment.isVoiceNote() ? DcMsg.DC_MSG_VOICE : DcMsg.DC_MSG_AUDIO);
+ } else if (MediaUtil.isVideoType(contentType) && slideDeck.getDocumentSlide() == null) {
+ msg = new DcMsg(dcContext, DcMsg.DC_MSG_VIDEO);
+ recompress = DcMsg.DC_MSG_VIDEO;
+ } else {
+ msg = new DcMsg(dcContext, DcMsg.DC_MSG_FILE);
+ }
+ String path = getRealPathFromAttachment(this, attachment);
+ msg.setFile(path, null);
}
- else if (MediaUtil.isAudioType(contentType)) {
- msg = new DcMsg(dcContext,
- attachment.isVoiceNote()? DcMsg.DC_MSG_VOICE : DcMsg.DC_MSG_AUDIO);
- }
- else if (MediaUtil.isVideoType(contentType) && slideDeck.getDocumentSlide()==null) {
- msg = new DcMsg(dcContext, DcMsg.DC_MSG_VIDEO);
- recompress = DcMsg.DC_MSG_VIDEO;
- }
- else {
- msg = new DcMsg(dcContext, DcMsg.DC_MSG_FILE);
- }
- String path = getRealPathFromAttachment(attachment);
- msg.setFile(path, null);
- msg.setText(body);
}
+ msg.setText(body);
}
catch(Exception e) {
e.printStackTrace();
@@ -1140,7 +1149,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DcMsg msg = (DcMsg)param[0];
Integer recompress = (Integer)param[1];
if (action==ACTION_SEND_OUT) {
- dcContext.setDraft(dcChat.getId(), null);
+
+ // for WEBXDC, drafts are just sent out as is.
+ // for preparations and other cases, cleanup draft soon.
+ if (msg.getType() != DcMsg.DC_MSG_WEBXDC) {
+ dcContext.setDraft(dcChat.getId(), null);
+ }
if(msg!=null)
{
@@ -1369,7 +1383,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void sendSticker(@NonNull Uri uri, String contentType) {
Attachment attachment = new UriAttachment(uri, null, contentType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, null, null, false);
- String path = getRealPathFromAttachment(attachment);
+ String path = getRealPathFromAttachment(this, attachment);
Optional quote = inputPanel.getQuote();
inputPanel.clearQuote();
diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java
index 8e83e134c..70a50d9ee 100644
--- a/src/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationItem.java
@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.QuoteView;
+import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
@@ -107,6 +108,7 @@ public class ConversationItem extends BaseConversationItem
private @NonNull Stub mediaThumbnailStub;
private @NonNull Stub audioViewStub;
private @NonNull Stub documentViewStub;
+ private @NonNull Stub webxdcViewStub;
private Stub stickerStub;
private @Nullable EventListener eventListener;
@@ -139,6 +141,7 @@ public class ConversationItem extends BaseConversationItem
this.mediaThumbnailStub = new Stub<>(findViewById(R.id.image_view_stub));
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
+ this.webxdcViewStub = new Stub<>(findViewById(R.id.webxdc_view_stub));
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
@@ -289,6 +292,11 @@ public class ConversationItem extends BaseConversationItem
documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
documentViewStub.get().setClickable(batchSelected.isEmpty());
}
+
+ if (webxdcViewStub.resolved()) {
+ webxdcViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
+ webxdcViewStub.get().setClickable(batchSelected.isEmpty());
+ }
}
private boolean hasAudio(DcMsg messageRecord) {
@@ -313,9 +321,14 @@ public class ConversationItem extends BaseConversationItem
return hasThumbnail(messageRecord) &&
!hasAudio(messageRecord) &&
!hasDocument(messageRecord) &&
+ !hasWebxdc(messageRecord) &&
!hasSticker(messageRecord);
}
+ private boolean hasWebxdc(DcMsg dcMsg) {
+ return dcMsg.getType()==DcMsg.DC_MSG_WEBXDC;
+ }
+
private boolean hasDocument(DcMsg dcMsg) {
return dcMsg.getType()==DcMsg.DC_MSG_FILE && !dcMsg.isSetupMessage();
}
@@ -361,6 +374,17 @@ public class ConversationItem extends BaseConversationItem
passthroughClickListener.onClick(view);
}
});
+ } else if (messageRecord.getType() == DcMsg.DC_MSG_WEBXDC) {
+ msgActionButton.setVisibility(View.VISIBLE);
+ msgActionButton.setEnabled(true);
+ msgActionButton.setText("Start…");
+ msgActionButton.setOnClickListener(view -> {
+ if (batchSelected.isEmpty()) {
+ WebxdcActivity.openWebxdcActivity(getContext(), messageRecord);
+ } else {
+ passthroughClickListener.onClick(view);
+ }
+ });
}
else if (messageRecord.hasHtml()) {
msgActionButton.setVisibility(View.VISIBLE);
@@ -401,6 +425,7 @@ public class ConversationItem extends BaseConversationItem
audioViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
+ if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
@@ -422,6 +447,7 @@ public class ConversationItem extends BaseConversationItem
documentViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
+ if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
@@ -433,10 +459,26 @@ public class ConversationItem extends BaseConversationItem
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
}
+ else if (hasWebxdc(messageRecord)) {
+ webxdcViewStub.get().setVisibility(View.VISIBLE);
+ if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
+ if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
+ if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
+ if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+
+ webxdcViewStub.get().setWebxdc(messageRecord);
+ webxdcViewStub.get().setWebxdcClickListener(new ThumbnailClickListener());
+ webxdcViewStub.get().setOnLongClickListener(passthroughClickListener);
+
+ ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ footer.setVisibility(VISIBLE);
+ }
else if (hasThumbnail(messageRecord)) {
mediaThumbnailStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
+ if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
Slide slide = MediaUtil.getSlideForMsg(context, messageRecord);
@@ -473,6 +515,7 @@ public class ConversationItem extends BaseConversationItem
stickerStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
+ if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
bodyBubble.setBackgroundColor(Color.TRANSPARENT);
@@ -492,6 +535,7 @@ public class ConversationItem extends BaseConversationItem
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
+ if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -725,6 +769,8 @@ public class ConversationItem extends BaseConversationItem
public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
+ } else if (messageRecord.getType() == DcMsg.DC_MSG_WEBXDC) {
+ WebxdcActivity.openWebxdcActivity(context, messageRecord);
} else if (MediaPreviewActivity.isTypeSupported(slide) && slide.getUri() != null) {
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
diff --git a/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java b/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java
index 10c928de3..0ad7cc88f 100644
--- a/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java
+++ b/src/org/thoughtcrime/securesms/ProfileDocumentsFragment.java
@@ -103,7 +103,7 @@ public class ProfileDocumentsFragment
@Override
public Loader onCreateLoader(int i, Bundle bundle) {
- return new BucketedThreadMediaLoader(getContext(), chatId, DcMsg.DC_MSG_FILE, DcMsg.DC_MSG_AUDIO, 0);
+ return new BucketedThreadMediaLoader(getContext(), chatId, DcMsg.DC_MSG_FILE, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_WEBXDC);
}
@Override
diff --git a/src/org/thoughtcrime/securesms/WebViewActivity.java b/src/org/thoughtcrime/securesms/WebViewActivity.java
index ebd9ddd52..29cfc1716 100644
--- a/src/org/thoughtcrime/securesms/WebViewActivity.java
+++ b/src/org/thoughtcrime/securesms/WebViewActivity.java
@@ -9,11 +9,14 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.Toast;
+import androidx.annotation.RequiresApi;
import androidx.appcompat.widget.SearchView;
import androidx.webkit.WebSettingsCompat;
import androidx.webkit.WebViewFeature;
@@ -75,6 +78,26 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
// if we come over really useful things, we should allow that explicitly.
return true;
}
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ WebResourceResponse res = interceptRequest(url);
+ if (res!=null) {
+ return res;
+ }
+ return super.shouldInterceptRequest(view, url);
+ }
+
+ @Override
+ @RequiresApi(21)
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
+ WebResourceResponse res = interceptRequest(request.getUrl().toString());
+ if (res!=null) {
+ return res;
+ }
+ return super.shouldInterceptRequest(view, request);
+ }
});
webView.setFindListener(this);
@@ -245,6 +268,10 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
}
}
+ protected WebResourceResponse interceptRequest(String url) {
+ return null;
+ }
+
@Override
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
super.onActivityResult(reqCode, resultCode, data);
diff --git a/src/org/thoughtcrime/securesms/WebxdcActivity.java b/src/org/thoughtcrime/securesms/WebxdcActivity.java
new file mode 100644
index 000000000..1688d0f24
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/WebxdcActivity.java
@@ -0,0 +1,172 @@
+package org.thoughtcrime.securesms;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.webkit.JavascriptInterface;
+import android.webkit.MimeTypeMap;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebSettings;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.b44t.messenger.DcContext;
+import com.b44t.messenger.DcEvent;
+import com.b44t.messenger.DcMsg;
+
+import org.json.JSONObject;
+import org.thoughtcrime.securesms.connect.DcEventCenter;
+import org.thoughtcrime.securesms.connect.DcHelper;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
+ private static final String TAG = WebxdcActivity.class.getSimpleName();
+ private static final String INTERNAL_SCHEMA = "webxdc";
+ private static final String INTERNAL_DOMAIN = "local.app";
+ private DcContext dcContext;
+ private DcMsg dcAppMsg;
+
+ public static void openWebxdcActivity(Context context, DcMsg instance) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Intent intent =new Intent(context, WebxdcActivity.class);
+ intent.putExtra("appMessageId", instance.getId());
+ context.startActivity(intent);
+ } else {
+ Toast.makeText(context, "At least Android 5.0 (Lollipop) required for Webxdc.", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle state, boolean ready) {
+ super.onCreate(state, ready);
+ DcEventCenter eventCenter = DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
+ eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
+
+ Bundle b = getIntent().getExtras();
+ int appMessageId = b.getInt("appMessageId");
+
+ this.dcContext = DcHelper.getContext(getApplicationContext());
+ this.dcAppMsg = this.dcContext.getMsg(appMessageId);
+
+ WebSettings webSettings = webView.getSettings();
+ webSettings.setJavaScriptEnabled(true);
+ webSettings.setAllowFileAccess(false);
+ webSettings.setBlockNetworkLoads(true);
+ webSettings.setAllowContentAccess(false);
+ webSettings.setGeolocationEnabled(false);
+ webSettings.setAllowFileAccessFromFileURLs(false);
+ webSettings.setAllowUniversalAccessFromFileURLs(false);
+ webView.addJavascriptInterface(new InternalJSApi(), "InternalJSApi");
+
+ webView.loadUrl(INTERNAL_SCHEMA + "://" + INTERNAL_DOMAIN + "/index.html");
+
+ Util.runOnAnyBackgroundThread(() -> {
+ JSONObject info = this.dcAppMsg.getWebxdcInfo();
+ Util.runOnMain(() -> {
+ try {
+ getSupportActionBar().setTitle(info.getString("name"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ DcHelper.getEventCenter(this.getApplicationContext()).removeObservers(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // do not call super.onPrepareOptionsMenu() as the default "Search" menu is not needed
+ return true;
+ }
+
+ @Override
+ protected void openOnlineUrl(String url) {
+ Toast.makeText(this, "Please embed needed resources.", Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ protected WebResourceResponse interceptRequest(String rawUrl) {
+ Log.i(TAG, "interceptRequest: " + rawUrl);
+ try {
+ if (rawUrl == null) {
+ throw new Exception("no url specified");
+ }
+ String path = Uri.parse(rawUrl).getPath();
+ if (path.equalsIgnoreCase("/webxdc.js")) {
+ InputStream targetStream = getResources().openRawResource(R.raw.webxdc);
+ return new WebResourceResponse("text/javascript", "UTF-8", targetStream);
+ } else {
+ byte[] blob = this.dcAppMsg.getWebxdcBlob(path);
+ if (blob == null) {
+ throw new Exception("\"" + path + "\" not found");
+ }
+ String ext = MimeTypeMap.getFileExtensionFromUrl(path);
+ String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
+ if (mimeType == null) {
+ switch (ext) {
+ case "js": mimeType = "text/javascript"; break;
+ default: mimeType = "application/octet-stream"; Log.i(TAG, "unknown mime type for " + rawUrl); break;
+ }
+ }
+ String encoding = mimeType.startsWith("text/")? "UTF-8" : null;
+ InputStream targetStream = new ByteArrayInputStream(blob);
+ return new WebResourceResponse(mimeType, encoding, targetStream);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ InputStream targetStream = new ByteArrayInputStream(("Webxdc Request Error: " + e.getMessage()).getBytes());
+ return new WebResourceResponse("text/plain", "UTF-8", targetStream);
+ }
+ }
+
+ @Override
+ public void handleEvent(@NonNull DcEvent event) {
+ int eventId = event.getId();
+ if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE && event.getData1Int() == dcAppMsg.getId())) {
+ Log.i(TAG, "handleEvent");
+ webView.loadUrl("javascript:window.__webxdcUpdate(" + event.getData2Int() + ");");
+ }
+ }
+
+ class InternalJSApi {
+ @JavascriptInterface
+ public String selfAddr() {
+ return WebxdcActivity.this.dcContext.getConfig("addr");
+ }
+
+ @JavascriptInterface
+ public String selfName() {
+ String name = WebxdcActivity.this.dcContext.getConfig("displayname");
+ if (TextUtils.isEmpty(name)) {
+ name = selfAddr();
+ }
+ return name;
+ }
+
+ @JavascriptInterface
+ public boolean sendStatusUpdate(String payload, String descr) {
+ Log.i(TAG, "sendStatusUpdate");
+ return WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate(WebxdcActivity.this.dcAppMsg.getId(), payload, descr);
+ }
+
+ @JavascriptInterface
+ public String getStatusUpdates(int statusUpdateId) {
+ Log.i(TAG, "getStatusUpdates");
+ return WebxdcActivity.this.dcContext.getWebxdcStatusUpdates(WebxdcActivity.this.dcAppMsg.getId(), statusUpdateId);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/WebxdcView.java b/src/org/thoughtcrime/securesms/components/WebxdcView.java
new file mode 100644
index 000000000..67b181824
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/WebxdcView.java
@@ -0,0 +1,102 @@
+package org.thoughtcrime.securesms.components;
+
+
+import android.content.Context;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageView;
+
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.b44t.messenger.DcMsg;
+
+import org.json.JSONObject;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.mms.DocumentSlide;
+import org.thoughtcrime.securesms.mms.SlideClickListener;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.io.ByteArrayInputStream;
+
+public class WebxdcView extends FrameLayout {
+
+ private static final String TAG = WebxdcView.class.getSimpleName();
+
+ private final @NonNull AppCompatImageView icon;
+ private final @NonNull TextView appName;
+ private final @NonNull TextView appSubtitle;
+
+ private @Nullable SlideClickListener viewListener;
+
+ public WebxdcView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public WebxdcView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WebxdcView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ inflate(context, R.layout.webxdc_view, this);
+
+ this.icon = findViewById(R.id.webxdc_icon);
+ this.appName = findViewById(R.id.webxdc_app_name);
+ this.appSubtitle = findViewById(R.id.webxdc_subtitle);
+ }
+
+ public void setWebxdcClickListener(@Nullable SlideClickListener listener) {
+ this.viewListener = listener;
+ }
+
+ public void setWebxdc(final @NonNull DcMsg dcMsg)
+ {
+ try {
+ JSONObject info = dcMsg.getWebxdcInfo();
+ setOnClickListener(new OpenClickedListener(getContext(), dcMsg));
+
+ // icon
+ byte[] blob = dcMsg.getWebxdcBlob(info.getString("icon"));
+ if (blob == null) {
+ throw new Exception("webxdc icon not found");
+ }
+ ByteArrayInputStream is = new ByteArrayInputStream(blob);
+ Drawable drawable = Drawable.createFromStream(is, "icon");
+ icon.setImageDrawable(drawable);
+
+ // name
+ appName.setText(info.getString("name"));
+
+ // subtitle
+ String summary = info.optString("summary");
+ if (summary.isEmpty()) {
+ summary = Util.getPrettyFileSize(dcMsg.getFilebytes()) + " Webxdc";
+ }
+ appSubtitle.setText(summary);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private class OpenClickedListener implements View.OnClickListener {
+ private final @NonNull DocumentSlide slide;
+
+ private OpenClickedListener(Context context, @NonNull DcMsg dcMsg) {
+ this.slide = new DocumentSlide(context, dcMsg);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (viewListener != null) {
+ viewListener.onClick(v, slide);
+ }
+ }
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
index e45fee21b..7d127bc2b 100644
--- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -38,17 +38,25 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.b44t.messenger.DcContext;
+import com.b44t.messenger.DcMsg;
+
import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareLocationDialog;
+import org.thoughtcrime.securesms.WebxdcActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView;
+import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.connect.DcHelper;
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
@@ -84,6 +92,7 @@ public class AttachmentManager {
private ThumbnailView thumbnail;
private AudioView audioView;
private DocumentView documentView;
+ private WebxdcView webxdcView;
//private SignalMapView mapView;
private @NonNull List garbage = new LinkedList<>();
@@ -104,6 +113,7 @@ public class AttachmentManager {
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
+ this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc);
//this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
@@ -113,6 +123,7 @@ public class AttachmentManager {
int incomingBubbleColor = ThemeUtil.getThemedColor(context, R.attr.conversation_item_incoming_bubble_color);
audioView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
documentView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
+ webxdcView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
}
}
@@ -220,9 +231,11 @@ public class AttachmentManager {
@SuppressLint("StaticFieldLeak")
public ListenableFuture setMedia(@NonNull final GlideRequests glideRequests,
@NonNull final Uri uri,
+ @Nullable final DcMsg msg,
@NonNull final MediaType mediaType,
final int width,
- final int height)
+ final int height,
+ final int chatId)
{
inflateStub();
@@ -238,10 +251,13 @@ public class AttachmentManager {
@Override
protected @Nullable Slide doInBackground(Void... params) {
try {
- if (PartAuthority.isLocalUri(uri)) {
+ if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) {
+ return new DocumentSlide(context, msg);
+ }
+ else if (PartAuthority.isLocalUri(uri)) {
return getManuallyCalculatedSlideInfo(uri, width, height);
} else {
- Slide result = getContentResolverSlideInfo(uri, width, height);
+ Slide result = getContentResolverSlideInfo(uri, width, height, chatId);
if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height);
else return result;
@@ -291,8 +307,17 @@ public class AttachmentManager {
removableMediaView.display(audioView, false);
result.set(true);
} else if (slide.hasDocument()) {
- documentView.setDocument((DocumentSlide) slide);
- removableMediaView.display(documentView, false);
+ if (slide.isWebxdcDocument()) {
+ DcMsg instance = msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId);
+ webxdcView.setWebxdc(instance);
+ webxdcView.setWebxdcClickListener((v, s) -> {
+ WebxdcActivity.openWebxdcActivity(context, instance);
+ });
+ removableMediaView.display(webxdcView, false);
+ } else {
+ documentView.setDocument((DocumentSlide) slide);
+ removableMediaView.display(documentView, false);
+ }
result.set(true);
} else {
Attachment attachment = slide.asAttachment();
@@ -304,7 +329,7 @@ public class AttachmentManager {
}
}
- private @Nullable Slide getContentResolverSlideInfo(Uri uri, int width, int height) {
+ private @Nullable Slide getContentResolverSlideInfo(Uri uri, int width, int height, int chatId) {
Cursor cursor = null;
long start = System.currentTimeMillis();
@@ -323,7 +348,7 @@ public class AttachmentManager {
}
Log.w(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
- return mediaType.createSlide(context, uri, fileName, mimeType, fileSize, width, height);
+ return mediaType.createSlide(context, uri, fileName, mimeType, fileSize, width, height, chatId);
}
} finally {
if (cursor != null) cursor.close();
@@ -367,7 +392,7 @@ public class AttachmentManager {
}
Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
- return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height);
+ return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height, chatId);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@@ -598,7 +623,8 @@ public class AttachmentManager {
@Nullable String mimeType,
long dataSize,
int width,
- int height)
+ int height,
+ int chatId)
{
if (mimeType == null) {
mimeType = "application/octet-stream";
@@ -609,7 +635,20 @@ public class AttachmentManager {
case GIF: return new GifSlide(context, uri, dataSize, width, height);
case AUDIO: return new AudioSlide(context, uri, dataSize, false, fileName);
case VIDEO: return new VideoSlide(context, uri, dataSize);
- case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
+ case DOCUMENT:
+ // We have to special-case Webxdc slides: The user can interact with them as soon as a draft
+ // is set. Therefore we need to create a DcMsg already now.
+ if (fileName != null && fileName.endsWith(".xdc")) {
+ DcContext dcContext = DcHelper.getContext(context);
+ DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC);
+ Attachment attachment = new UriAttachment(uri, null, MediaUtil.WEBXDC, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, fileName, null, false);
+ String path = ConversationActivity.getRealPathFromAttachment(context, attachment);
+ msg.setFile(path, MediaUtil.WEBXDC);
+ dcContext.setDraft(chatId, msg);
+ return new DocumentSlide(context, msg);
+ } else {
+ return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
+ }
default: throw new AssertionError("unrecognized enum");
}
}
diff --git a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java
index 6328ff745..6e3cc3686 100644
--- a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java
@@ -13,10 +13,12 @@ import org.thoughtcrime.securesms.attachments.DcAttachment;
import org.thoughtcrime.securesms.util.StorageUtil;
public class DocumentSlide extends Slide {
+ private int dcMsgType = DcMsg.DC_MSG_UNDEFINED;
public DocumentSlide(Context context, DcMsg dcMsg) {
super(context, new DcAttachment(dcMsg));
dcMsgId = dcMsg.getId();
+ dcMsgType = dcMsg.getType();
}
public DocumentSlide(@NonNull Context context, @NonNull Uri uri,
@@ -31,4 +33,8 @@ public class DocumentSlide extends Slide {
return true;
}
+ @Override
+ public boolean isWebxdcDocument() {
+ return dcMsgType == DcMsg.DC_MSG_WEBXDC;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java
index 66865a4c1..c8f32df9f 100644
--- a/src/org/thoughtcrime/securesms/mms/Slide.java
+++ b/src/org/thoughtcrime/securesms/mms/Slide.java
@@ -94,6 +94,10 @@ public abstract class Slide {
return false;
}
+ public boolean isWebxdcDocument() {
+ return false;
+ }
+
public boolean hasLocation() {
return false;
}
diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
index cff4708a1..7fe36e5f5 100644
--- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java
+++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
@@ -19,6 +19,8 @@ package org.thoughtcrime.securesms.mms;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.b44t.messenger.DcMsg;
+
import org.thoughtcrime.securesms.attachments.Attachment;
import java.util.LinkedList;
@@ -72,4 +74,14 @@ public class SlideDeck {
return null;
}
+
+ // Webxdc requires draft-ids to be used; this function returns the previously used draft-id, if any.
+ public int getWebxdctDraftId() {
+ for (Slide slide: slides) {
+ if (slide.isWebxdcDocument()) {
+ return slide.dcMsgId;
+ }
+ }
+ return 0;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java
index 756eb82a3..eaa36b035 100644
--- a/src/org/thoughtcrime/securesms/util/MediaUtil.java
+++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java
@@ -47,6 +47,7 @@ public class MediaUtil {
public static final String AUDIO_UNSPECIFIED = "audio/*";
public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String OCTET = "application/octet-stream";
+ public static final String WEBXDC = "application/webxdc+zip";
public static Slide getSlideForMsg(Context context, DcMsg dcMsg) {
@@ -62,7 +63,8 @@ public class MediaUtil {
} else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO
|| dcMsg.getType() == DcMsg.DC_MSG_VOICE) {
slide = new AudioSlide(context, dcMsg);
- } else if (dcMsg.getType() == DcMsg.DC_MSG_FILE) {
+ } else if (dcMsg.getType() == DcMsg.DC_MSG_FILE
+ || dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) {
slide = new DocumentSlide(context, dcMsg);
}
@@ -259,6 +261,8 @@ public class MediaUtil {
return "aac";
case IMAGE_WEBP:
return "webp";
+ case WEBXDC:
+ return "xdc";
}
return null;
}