* add new w30 APIs

* create the webview,
disable internet access,
inject deltachat.js

* connect deltachat to the webview

* promisify api

* use msgActionButton to start the w30 apps

* cleanup

- create observers in onCreate() to avoid memory leak,
- derive from WebViewActivity to easier deal with particularities
  and to saves >100 loc
- reorder some methods to reflect lifetimes

* make it more clear, which uri-part is 'domain' and which one is 'path'

* unify logging

* it is 'statusUpdate' not 'stateUpdate'; not sure if promise is needed at the end, we can readd that as needed, simple code for now

* use core implementation for status updates

* disable debugging enabled by default, streamline code

* use same name for InternalJSApi for both, js and java

* getStatusUpdates() always return an array

* call JSON.stringify() on payload

* fix typo, fix equal operator

* use shorter function names in js land

* adapt to new zipped w30 format

* load any file from w30 archives

* add fallback if getMimeTypeFromExtension() fails

* rename w30 to webxdc

* add selfName()

* return selfAddr() if selfName() is empty

* rename deltachat.js to webxdc.js

* observer correct event

* rename getBlobFromArchive() to getWebxdcBlob()

* show webxdc app name in title bar

* swap payload and descr in sendUpdate() (adapt to new core api)

* allow user-defined-texts for webxdc apps, make room for icon+name

* show webxdc icon and name in chats

* render webxdc drafts accordingly

* allow configuring drafts

* do not destroy webxdc-message to be sent out by removing it via setDraft(null)

* fix crash when replying to webxdc messages

* add webxdc messages to profile's document tab

* hide 'search menu' for webxdc apps

* show app summary beside app icon

* remove outdated comment

* add precautious WebView restrictions

* Update src/org/thoughtcrime/securesms/ConversationItem.java

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

* Update src/org/thoughtcrime/securesms/ConversationItem.java

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

* Update src/org/thoughtcrime/securesms/ConversationActivity.java

Co-authored-by: Hocuri <hocuri@gmx.de>

* Webxdc requires at least Android 5 Lollipop

see https://github.com/deltachat/deltachat-android/pull/2174#discussion_r785436874

* recognize .xdc files on android10

based on @Hocuri's findings in #2188

Co-authored-by: Simon Laux <mobile.info@simonlaux.de>
Co-authored-by: adbenitez <asieldbenitez@gmail.com>
Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>
Co-authored-by: Hocuri <hocuri@gmx.de>
This commit is contained in:
bjoern 2022-01-18 10:52:09 +01:00 committed by GitHub
parent aaf53e71a0
commit 9e70aa7cad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 665 additions and 39 deletions

View file

@ -277,6 +277,11 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
</activity> </activity>
<activity android:name=".WebxdcActivity"
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
</activity>
<activity android:name=".FullMsgActivity" <activity android:name=".FullMsgActivity"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">

View file

@ -86,6 +86,19 @@ static jstring jstring_new__(JNIEnv* env, const char* a)
#define CTIMESTAMP(a) (((jlong)a)/((jlong)1000)) #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) 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. /* 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) JNIEXPORT jint Java_com_b44t_messenger_DcContext_addDeviceMsg(JNIEnv *env, jobject obj, jstring label, jobject msg)
{ {
CHAR_REF(label); 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) JNIEXPORT jboolean Java_com_b44t_messenger_DcMsg_isForwarded(JNIEnv *env, jobject obj)
{ {
return dc_msg_is_forwarded(get_dc_msg(env, obj))!=0; return dc_msg_is_forwarded(get_dc_msg(env, obj))!=0;

View file

@ -58,6 +58,17 @@
android:padding="8dp" android:padding="8dp"
android:background="@drawable/message_bubble_background_sent_alone"/> android:background="@drawable/message_bubble_background_sent_alone"/>
<org.thoughtcrime.securesms.components.WebxdcView
android:id="@+id/attachment_webxdc"
android:layout_width="230dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:padding="8dp"
android:background="@drawable/message_bubble_background_sent_alone"/>
</org.thoughtcrime.securesms.components.RemovableEditableMediaView> </org.thoughtcrime.securesms.components.RemovableEditableMediaView>
</FrameLayout> </FrameLayout>

View file

@ -143,6 +143,16 @@
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding" android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" /> android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/webxdc_view_stub"
android:layout="@layout/conversation_item_webxdc"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -121,6 +121,16 @@
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding" android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" /> android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/webxdc_view_stub"
android:layout="@layout/conversation_item_webxdc"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.WebxdcView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/webxdc_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"/>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="org.thoughtcrime.securesms.components.WebxdcView">
<LinearLayout android:id="@+id/webxdc_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/webxdc_icon"
android:layout_marginTop="2dp"
android:layout_width="72dp"
android:layout_height="72dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_chevron_up"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginLeft="6dp"
android:layout_marginStart="6dp"
android:orientation="vertical"
android:focusable="false"
android:clickable="false">
<TextView android:id="@+id/webxdc_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Signal.Text.Body"
android:singleLine="true"
android:maxLines="1"
android:clickable="false"
android:ellipsize="end"
android:textColor="?conversation_item_incoming_text_primary_color"
android:textStyle="bold"
tools:text="Great App"/>
<TextView android:id="@+id/webxdc_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
style="@style/Signal.Text.Caption"
android:clickable="false"
android:textColor="?conversation_item_incoming_text_primary_color"
tools:text="24kb"/>
</LinearLayout>
</LinearLayout>
</merge>

26
res/raw/webxdc.js Normal file
View file

@ -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);
},
};
})();

View file

@ -27,6 +27,7 @@ public class DcContext {
public final static int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061; 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_CONNECTIVITY_CHANGED = 2100;
public final static int DC_EVENT_SELFAVATAR_CHANGED = 2110; 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_EXPORT_SELF_KEYS = 1;
public final static int DC_IMEX_IMPORT_SELF_KEYS = 2; 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 sendMsg (int chat_id, DcMsg msg);
public native int sendTextMsg (int chat_id, String text); public native int sendTextMsg (int chat_id, String text);
public native int sendVideochatInvitation(int chat_id); 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 int addDeviceMsg (String label, DcMsg msg);
public native boolean wasDeviceMsgEverAdded(String label); public native boolean wasDeviceMsgEverAdded(String label);
public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); } public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); }

View file

@ -1,5 +1,8 @@
package com.b44t.messenger; package com.b44t.messenger;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File; import java.io.File;
import java.util.Set; 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_VIDEO = 50;
public final static int DC_MSG_FILE = 60; public final static int DC_MSG_FILE = 60;
public final static int DC_MSG_VIDEOCHAT_INVITATION = 70; 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_UNDEFINED = 0;
public final static int DC_STATE_IN_FRESH = 10; 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_NOTICED = 13;
public final static int DC_STATE_IN_SEEN = 16; 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_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_PENDING = 20;
public final static int DC_STATE_OUT_ERROR = 24; public final static int DC_STATE_OUT_ERROR = 24;
public final static int DC_STATE_OUT_DELIVERED = 26; public final static int DC_STATE_OUT_DELIVERED = 26;
@ -110,6 +115,15 @@ public class DcMsg {
public native String getFilemime (); public native String getFilemime ();
public native String getFilename (); public native String getFilename ();
public native long getFilebytes (); 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 isForwarded ();
public native boolean isInfo (); public native boolean isInfo ();
public native boolean isSetupMessage (); public native boolean isSetupMessage ();
@ -206,4 +220,5 @@ public class DcMsg {
private native long getSummaryCPtr (long chatCPtr); private native long getSummaryCPtr (long chatCPtr);
private native void setQuoteCPtr (long quoteCPtr); private native void setQuoteCPtr (long quoteCPtr);
private native long getQuotedMsgCPtr (); private native long getQuotedMsgCPtr ();
private native String getWebxdcInfoJson ();
}; };

View file

@ -46,6 +46,7 @@ import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener; import android.view.View.OnKeyListener;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.webkit.MimeTypeMap;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@ -836,6 +837,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case DcMsg.DC_MSG_VIDEO: case DcMsg.DC_MSG_VIDEO:
setMedia(uri, MediaType.VIDEO).addListener(listener); setMedia(uri, MediaType.VIDEO).addListener(listener);
break; break;
case DcMsg.DC_MSG_WEBXDC:
setMedia(draft, MediaType.DOCUMENT).addListener(listener);
break;
default: default:
setMedia(uri, MediaType.DOCUMENT).addListener(listener); setMedia(uri, MediaType.DOCUMENT).addListener(listener);
break; break;
@ -1012,7 +1016,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(false); return new SettableFuture<>(false);
} }
return attachmentManager.setMedia(glideRequests, uri, mediaType, 0, 0); return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
}
private ListenableFuture<Boolean> 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) { private void addAttachmentContactInfo(Intent data) {
@ -1029,7 +1037,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return dcChat.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED; return dcChat.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED;
} }
private String getRealPathFromAttachment(Attachment attachment) { public static String getRealPathFromAttachment(Context context, Attachment attachment) {
try { try {
// get file in the blobdir as `<blobdir>/<name>[-<uniqueNumber>].<ext>` // get file in the blobdir as `<blobdir>/<name>[-<uniqueNumber>].<ext>`
String filename = attachment.getFileName(); String filename = attachment.getFileName();
@ -1045,11 +1053,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
filename = filename.substring(0, i); 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 // copy content to this file
if(path!=null) { if(path!=null) {
InputStream inputStream = PartAuthority.getAttachmentStream(this, attachment.getDataUri()); InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
OutputStream outputStream = new FileOutputStream(path); OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream); Util.copy(inputStream, outputStream);
} }
@ -1095,29 +1103,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
try { try {
List<Attachment> attachments = slideDeck.asAttachments(); if (slideDeck.getWebxdctDraftId() != 0) {
for (Attachment attachment : attachments) { msg = dcContext.getDraft(chatId);
String contentType = attachment.getContentType(); } else {
if (MediaUtil.isImageType(contentType) && slideDeck.getDocumentSlide()==null) { List<Attachment> attachments = slideDeck.asAttachments();
msg = new DcMsg(dcContext, for (Attachment attachment : attachments) {
MediaUtil.isGif(contentType) ? DcMsg.DC_MSG_GIF : DcMsg.DC_MSG_IMAGE); String contentType = attachment.getContentType();
msg.setDimension(attachment.getWidth(), attachment.getHeight()); 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) { catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -1140,7 +1149,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DcMsg msg = (DcMsg)param[0]; DcMsg msg = (DcMsg)param[0];
Integer recompress = (Integer)param[1]; Integer recompress = (Integer)param[1];
if (action==ACTION_SEND_OUT) { 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) if(msg!=null)
{ {
@ -1369,7 +1383,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void sendSticker(@NonNull Uri uri, String contentType) { private void sendSticker(@NonNull Uri uri, String contentType) {
Attachment attachment = new UriAttachment(uri, null, contentType, Attachment attachment = new UriAttachment(uri, null, contentType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, null, null, false); AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, null, null, false);
String path = getRealPathFromAttachment(attachment); String path = getRealPathFromAttachment(this, attachment);
Optional<QuoteModel> quote = inputPanel.getQuote(); Optional<QuoteModel> quote = inputPanel.getQuote();
inputPanel.clearQuote(); inputPanel.clearQuote();

View file

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail; import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.QuoteView; import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
@ -107,6 +108,7 @@ public class ConversationItem extends BaseConversationItem
private @NonNull Stub<ConversationItemThumbnail> mediaThumbnailStub; private @NonNull Stub<ConversationItemThumbnail> mediaThumbnailStub;
private @NonNull Stub<AudioView> audioViewStub; private @NonNull Stub<AudioView> audioViewStub;
private @NonNull Stub<DocumentView> documentViewStub; private @NonNull Stub<DocumentView> documentViewStub;
private @NonNull Stub<WebxdcView> webxdcViewStub;
private Stub<BorderlessImageView> stickerStub; private Stub<BorderlessImageView> stickerStub;
private @Nullable EventListener eventListener; private @Nullable EventListener eventListener;
@ -139,6 +141,7 @@ public class ConversationItem extends BaseConversationItem
this.mediaThumbnailStub = new Stub<>(findViewById(R.id.image_view_stub)); this.mediaThumbnailStub = new Stub<>(findViewById(R.id.image_view_stub));
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub)); this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
this.documentViewStub = new Stub<>(findViewById(R.id.document_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.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
this.groupSenderHolder = findViewById(R.id.group_sender_holder); this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view); 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().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
documentViewStub.get().setClickable(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) { private boolean hasAudio(DcMsg messageRecord) {
@ -313,9 +321,14 @@ public class ConversationItem extends BaseConversationItem
return hasThumbnail(messageRecord) && return hasThumbnail(messageRecord) &&
!hasAudio(messageRecord) && !hasAudio(messageRecord) &&
!hasDocument(messageRecord) && !hasDocument(messageRecord) &&
!hasWebxdc(messageRecord) &&
!hasSticker(messageRecord); !hasSticker(messageRecord);
} }
private boolean hasWebxdc(DcMsg dcMsg) {
return dcMsg.getType()==DcMsg.DC_MSG_WEBXDC;
}
private boolean hasDocument(DcMsg dcMsg) { private boolean hasDocument(DcMsg dcMsg) {
return dcMsg.getType()==DcMsg.DC_MSG_FILE && !dcMsg.isSetupMessage(); return dcMsg.getType()==DcMsg.DC_MSG_FILE && !dcMsg.isSetupMessage();
} }
@ -361,6 +374,17 @@ public class ConversationItem extends BaseConversationItem
passthroughClickListener.onClick(view); 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()) { else if (messageRecord.hasHtml()) {
msgActionButton.setVisibility(View.VISIBLE); msgActionButton.setVisibility(View.VISIBLE);
@ -401,6 +425,7 @@ public class ConversationItem extends BaseConversationItem
audioViewStub.get().setVisibility(View.VISIBLE); audioViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.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); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions //noinspection ConstantConditions
@ -422,6 +447,7 @@ public class ConversationItem extends BaseConversationItem
documentViewStub.get().setVisibility(View.VISIBLE); documentViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.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); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions //noinspection ConstantConditions
@ -433,10 +459,26 @@ public class ConversationItem extends BaseConversationItem
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE); 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)) { else if (hasThumbnail(messageRecord)) {
mediaThumbnailStub.get().setVisibility(View.VISIBLE); mediaThumbnailStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.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); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
Slide slide = MediaUtil.getSlideForMsg(context, messageRecord); Slide slide = MediaUtil.getSlideForMsg(context, messageRecord);
@ -473,6 +515,7 @@ public class ConversationItem extends BaseConversationItem
stickerStub.get().setVisibility(View.VISIBLE); stickerStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.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); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
bodyBubble.setBackgroundColor(Color.TRANSPARENT); bodyBubble.setBackgroundColor(Color.TRANSPARENT);
@ -492,6 +535,7 @@ public class ConversationItem extends BaseConversationItem
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.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(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParams(groupSenderHolder, 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) { public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick(); performClick();
} else if (messageRecord.getType() == DcMsg.DC_MSG_WEBXDC) {
WebxdcActivity.openWebxdcActivity(context, messageRecord);
} else if (MediaPreviewActivity.isTypeSupported(slide) && slide.getUri() != null) { } else if (MediaPreviewActivity.isTypeSupported(slide) && slide.getUri() != null) {
Intent intent = new Intent(context, MediaPreviewActivity.class); Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

View file

@ -103,7 +103,7 @@ public class ProfileDocumentsFragment
@Override @Override
public Loader<BucketedThreadMediaLoader.BucketedThreadMedia> onCreateLoader(int i, Bundle bundle) { public Loader<BucketedThreadMediaLoader.BucketedThreadMedia> 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 @Override

View file

@ -9,11 +9,14 @@ import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SearchView;
import androidx.webkit.WebSettingsCompat; import androidx.webkit.WebSettingsCompat;
import androidx.webkit.WebViewFeature; 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. // if we come over really useful things, we should allow that explicitly.
return true; 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); webView.setFindListener(this);
@ -245,6 +268,10 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
} }
} }
protected WebResourceResponse interceptRequest(String url) {
return null;
}
@Override @Override
public void onActivityResult(int reqCode, int resultCode, final Intent data) { public void onActivityResult(int reqCode, int resultCode, final Intent data) {
super.onActivityResult(reqCode, resultCode, data); super.onActivityResult(reqCode, resultCode, data);

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -38,17 +38,25 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareLocationDialog; import org.thoughtcrime.securesms.ShareLocationDialog;
import org.thoughtcrime.securesms.WebxdcActivity;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer; import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.geolocation.DcLocationManager; import org.thoughtcrime.securesms.geolocation.DcLocationManager;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
@ -84,6 +92,7 @@ public class AttachmentManager {
private ThumbnailView thumbnail; private ThumbnailView thumbnail;
private AudioView audioView; private AudioView audioView;
private DocumentView documentView; private DocumentView documentView;
private WebxdcView webxdcView;
//private SignalMapView mapView; //private SignalMapView mapView;
private @NonNull List<Uri> garbage = new LinkedList<>(); private @NonNull List<Uri> garbage = new LinkedList<>();
@ -104,6 +113,7 @@ public class AttachmentManager {
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail); this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(root, R.id.attachment_audio); this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
this.documentView = ViewUtil.findById(root, R.id.attachment_document); 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.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view); 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); int incomingBubbleColor = ThemeUtil.getThemedColor(context, R.attr.conversation_item_incoming_bubble_color);
audioView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); audioView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
documentView.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") @SuppressLint("StaticFieldLeak")
public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests, public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests,
@NonNull final Uri uri, @NonNull final Uri uri,
@Nullable final DcMsg msg,
@NonNull final MediaType mediaType, @NonNull final MediaType mediaType,
final int width, final int width,
final int height) final int height,
final int chatId)
{ {
inflateStub(); inflateStub();
@ -238,10 +251,13 @@ public class AttachmentManager {
@Override @Override
protected @Nullable Slide doInBackground(Void... params) { protected @Nullable Slide doInBackground(Void... params) {
try { 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); return getManuallyCalculatedSlideInfo(uri, width, height);
} else { } else {
Slide result = getContentResolverSlideInfo(uri, width, height); Slide result = getContentResolverSlideInfo(uri, width, height, chatId);
if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height); if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height);
else return result; else return result;
@ -291,8 +307,17 @@ public class AttachmentManager {
removableMediaView.display(audioView, false); removableMediaView.display(audioView, false);
result.set(true); result.set(true);
} else if (slide.hasDocument()) { } else if (slide.hasDocument()) {
documentView.setDocument((DocumentSlide) slide); if (slide.isWebxdcDocument()) {
removableMediaView.display(documentView, false); 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); result.set(true);
} else { } else {
Attachment attachment = slide.asAttachment(); 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; Cursor cursor = null;
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
@ -323,7 +348,7 @@ public class AttachmentManager {
} }
Log.w(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms"); 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 { } finally {
if (cursor != null) cursor.close(); 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"); 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); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -598,7 +623,8 @@ public class AttachmentManager {
@Nullable String mimeType, @Nullable String mimeType,
long dataSize, long dataSize,
int width, int width,
int height) int height,
int chatId)
{ {
if (mimeType == null) { if (mimeType == null) {
mimeType = "application/octet-stream"; mimeType = "application/octet-stream";
@ -609,7 +635,20 @@ public class AttachmentManager {
case GIF: return new GifSlide(context, uri, dataSize, width, height); case GIF: return new GifSlide(context, uri, dataSize, width, height);
case AUDIO: return new AudioSlide(context, uri, dataSize, false, fileName); case AUDIO: return new AudioSlide(context, uri, dataSize, false, fileName);
case VIDEO: return new VideoSlide(context, uri, dataSize); 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"); default: throw new AssertionError("unrecognized enum");
} }
} }

View file

@ -13,10 +13,12 @@ import org.thoughtcrime.securesms.attachments.DcAttachment;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.StorageUtil;
public class DocumentSlide extends Slide { public class DocumentSlide extends Slide {
private int dcMsgType = DcMsg.DC_MSG_UNDEFINED;
public DocumentSlide(Context context, DcMsg dcMsg) { public DocumentSlide(Context context, DcMsg dcMsg) {
super(context, new DcAttachment(dcMsg)); super(context, new DcAttachment(dcMsg));
dcMsgId = dcMsg.getId(); dcMsgId = dcMsg.getId();
dcMsgType = dcMsg.getType();
} }
public DocumentSlide(@NonNull Context context, @NonNull Uri uri, public DocumentSlide(@NonNull Context context, @NonNull Uri uri,
@ -31,4 +33,8 @@ public class DocumentSlide extends Slide {
return true; return true;
} }
@Override
public boolean isWebxdcDocument() {
return dcMsgType == DcMsg.DC_MSG_WEBXDC;
}
} }

View file

@ -94,6 +94,10 @@ public abstract class Slide {
return false; return false;
} }
public boolean isWebxdcDocument() {
return false;
}
public boolean hasLocation() { public boolean hasLocation() {
return false; return false;
} }

View file

@ -19,6 +19,8 @@ package org.thoughtcrime.securesms.mms;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import java.util.LinkedList; import java.util.LinkedList;
@ -72,4 +74,14 @@ public class SlideDeck {
return null; 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;
}
} }

View file

@ -47,6 +47,7 @@ public class MediaUtil {
public static final String AUDIO_UNSPECIFIED = "audio/*"; public static final String AUDIO_UNSPECIFIED = "audio/*";
public static final String VIDEO_UNSPECIFIED = "video/*"; public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String OCTET = "application/octet-stream"; public static final String OCTET = "application/octet-stream";
public static final String WEBXDC = "application/webxdc+zip";
public static Slide getSlideForMsg(Context context, DcMsg dcMsg) { public static Slide getSlideForMsg(Context context, DcMsg dcMsg) {
@ -62,7 +63,8 @@ public class MediaUtil {
} else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO } else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO
|| dcMsg.getType() == DcMsg.DC_MSG_VOICE) { || dcMsg.getType() == DcMsg.DC_MSG_VOICE) {
slide = new AudioSlide(context, dcMsg); 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); slide = new DocumentSlide(context, dcMsg);
} }
@ -259,6 +261,8 @@ public class MediaUtil {
return "aac"; return "aac";
case IMAGE_WEBP: case IMAGE_WEBP:
return "webp"; return "webp";
case WEBXDC:
return "xdc";
} }
return null; return null;
} }