Detailed changes
@@ -18,13 +18,15 @@ import android.view.LayoutInflater;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
import android.webkit.JavascriptInterface;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.RequiresApi;
@@ -64,6 +66,7 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.ConversationsActivity;
+import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
@@ -77,10 +80,12 @@ public class WebxdcPage implements ConversationPage {
protected String baseUrl;
protected Message source;
protected WebxdcUpdate lastUpdate = null;
+ protected WeakReference<XmppActivity> activity;
- public WebxdcPage(Cid cid, Message source, XmppConnectionService xmppConnectionService) {
+ public WebxdcPage(final XmppActivity activity, Cid cid, Message source, XmppConnectionService xmppConnectionService) {
this.xmppConnectionService = xmppConnectionService;
this.source = source;
+ this.activity = new WeakReference(activity);
File f = xmppConnectionService.getFileForCid(cid);
try {
if (f != null) zip = new ZipFile(xmppConnectionService.getFileForCid(cid));
@@ -252,6 +257,20 @@ public class WebxdcPage implements ConversationPage {
}
});
+ binding.webview.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
+ // WebxdcActivity.this.filePathCallback = filePathCallback;
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE);
+ final XmppActivity activity = WebxdcPage.this.activity.get();
+ if (activity != null) activity.startActivityWithCallback(Intent.createChooser(intent, "Choose a file"), filePathCallback);
+ return activity != null;
+ }
+ });
+
// disable "safe browsing" as this has privacy issues,
// eg. at least false positives are sent to the "Safe Browsing Lookup API".
// as all URLs opened in the WebView are local anyway,
@@ -402,5 +421,36 @@ public class WebxdcPage implements ConversationPage {
builder.append("]");
return builder.toString();
}
+
+ @JavascriptInterface
+ public String sendToChat(String message) {
+ try {
+ JSONObject jsonObject = new JSONObject(message);
+
+ String text = null;
+ String data = null;
+ String name = null;
+ if (jsonObject.has("base64")) {
+ data = jsonObject.getString("base64");
+ }
+ if (jsonObject.has("name")) {
+ name = jsonObject.getString("name");
+ }
+ if (jsonObject.has("text")) {
+ text = jsonObject.getString("text");
+ }
+
+ Intent intent = new Intent(xmppConnectionService, ConversationsActivity.class);
+ intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
+ intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, ((Conversation) source.getConversation()).getUuid());
+ if (text != null) intent.putExtra(Intent.EXTRA_TEXT, text);
+ if (data != null) intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("data:application/octet-stream;base64," + data));
+ activity.get().startActivity(intent);
+ return null;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return e.toString();
+ }
+ }
}
}
@@ -36,5 +36,82 @@ window.webxdc = (() => {
sendUpdate: (payload, descr) => {
InternalJSApi.sendStatusUpdate(JSON.stringify(payload), descr);
},
+
+ importFiles: (filters) => {
+ var element = document.createElement("input");
+ element.type = "file";
+ element.accept = [
+ ...(filters.extensions || []),
+ ...(filters.mimeTypes || []),
+ ].join(",");
+ element.multiple = filters.multiple || false;
+ const promise = new Promise((resolve, _reject) => {
+ element.onchange = (_ev) => {
+ const files = Array.from(element.files || []);
+ document.body.removeChild(element);
+ resolve(files);
+ };
+ });
+ element.style.display = "none";
+ document.body.appendChild(element);
+ element.click();
+ return promise;
+ },
+
+ sendToChat: async (message) => {
+ const data = {};
+ if (!message.file && !message.text) {
+ return Promise.reject("sendToChat() error: file or text missing");
+ }
+ const blobToBase64 = (file) => {
+ const dataStart = ";base64,";
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ let data = reader.result;
+ resolve(data.slice(data.indexOf(dataStart) + dataStart.length));
+ };
+ reader.onerror = () => reject(reader.error);
+ });
+ };
+ if (message.text) {
+ data.text = message.text;
+ }
+
+ if (message.file) {
+ let base64content;
+ if (!message.file.name) {
+ return Promise.reject("sendToChat() error: file name missing");
+ }
+ if (
+ Object.keys(message.file).filter((key) =>
+ ["blob", "base64", "plainText"].includes(key)
+ ).length > 1
+ ) {
+ return Promise.reject("sendToChat() error: only one of blob, base64 or plainText allowed");
+ }
+
+ if (message.file.blob instanceof Blob) {
+ base64content = await blobToBase64(message.file.blob);
+ } else if (typeof message.file.base64 === "string") {
+ base64content = message.file.base64;
+ } else if (typeof message.file.plainText === "string") {
+ base64content = await blobToBase64(
+ new Blob([message.file.plainText])
+ );
+ } else {
+ return Promise.reject("sendToChat() error: none of blob, base64 or plainText set correctly");
+ }
+ data.base64 = base64content;
+ data.name = message.file.name;
+ }
+
+ const errorMsg = InternalJSApi.sendToChat(JSON.stringify(data));
+ if (errorMsg) {
+ return Promise.reject(errorMsg);
+ }
+ },
+
};
})();
@@ -53,6 +53,7 @@ import com.madebyevan.thumbhash.ThumbHash;
import com.wolt.blurhashkt.BlurHashDecoder;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@@ -713,6 +714,26 @@ public class FileBackend {
}
}
+ private InputStream openInputStream(Uri uri) throws IOException {
+ if (uri != null && "data".equals(uri.getScheme())) {
+ String[] parts = uri.getSchemeSpecificPart().split(",", 2);
+ byte[] data;
+ if (Arrays.asList(parts[0].split(";")).contains("base64")) {
+ String[] parts2 = parts[0].split(";", 2);
+ parts[0] = parts2[0];
+ data = Base64.decode(parts[1], 0);
+ } else {
+ try {
+ data = parts[1].getBytes("UTF-8");
+ } catch (final IOException e) {
+ data = new byte[0];
+ }
+ }
+ return new ByteArrayInputStream(data);
+ }
+ return mXmppConnectionService.getContentResolver().openInputStream(uri);
+ }
+
private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
Log.d(
Config.LOGTAG,
@@ -724,8 +745,7 @@ public class FileBackend {
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
}
try (final OutputStream os = new FileOutputStream(file);
- final InputStream is =
- mXmppConnectionService.getContentResolver().openInputStream(uri)) {
+ final InputStream is = openInputStream(uri)) {
if (is == null) {
throw new FileCopyException(R.string.error_file_not_found);
}
@@ -951,7 +971,7 @@ public class FileBackend {
public void setupRelativeFilePath(final Message message, final Uri uri, final String extension) throws FileCopyException, XmppConnectionService.BlockedMediaException {
try {
- setupRelativeFilePath(message, mXmppConnectionService.getContentResolver().openInputStream(uri), extension);
+ setupRelativeFilePath(message, openInputStream(uri), extension);
} catch (final FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found);
} catch (final IOException e) {
@@ -3214,7 +3214,7 @@ public class ConversationFragment extends XmppFragment
if (message == null) return;
Cid webxdcCid = message.getFileParams().getCids().get(0);
- WebxdcPage webxdc = new WebxdcPage(webxdcCid, message, activity.xmppConnectionService);
+ WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
Conversation conversation = (Conversation) message.getConversation();
if (!conversation.switchToSession("webxdc\0" + message.getUuid())) {
conversation.startWebxdc(webxdc);
@@ -41,9 +41,11 @@ import android.text.Html;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.webkit.ValueCallback;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
@@ -65,6 +67,7 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.PriorityQueue;
import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.Config;
@@ -117,6 +120,7 @@ public abstract class XmppActivity extends ActionBarActivity {
protected Toast mToast;
public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
protected ConferenceInvite mPendingConferenceInvite = null;
+ protected PriorityQueue<Pair<Integer, ValueCallback<Uri[]>>> activityCallbacks = new PriorityQueue<>((x, y) -> y.first.compareTo(x.first));
protected ServiceConnection mConnection = new ServiceConnection() {
@Override
@@ -854,6 +858,13 @@ public abstract class XmppActivity extends ActionBarActivity {
}
}
+ public synchronized void startActivityWithCallback(Intent intent, ValueCallback<Uri[]> cb) {
+ Pair<Integer, ValueCallback<Uri[]>> peek = activityCallbacks.peek();
+ int index = peek == null ? 1 : peek.first + 1;
+ activityCallbacks.add(new Pair<>(index, cb));
+ startActivityForResult(intent, index);
+ }
+
protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
@@ -865,6 +876,21 @@ public abstract class XmppActivity extends ActionBarActivity {
}
mPendingConferenceInvite = null;
}
+ } else if (resultCode == RESULT_OK) {
+ for (Pair<Integer, ValueCallback<Uri[]>> cb : new ArrayList<>(activityCallbacks)) {
+ if (cb.first == requestCode) {
+ activityCallbacks.remove(cb);
+ ArrayList<Uri> dataUris = new ArrayList<>();
+ if (data.getDataString() != null) {
+ dataUris.add(Uri.parse(data.getDataString()));
+ } else if (data.getClipData() != null) {
+ for (int i = 0; i < data.getClipData().getItemCount(); i++) {
+ dataUris.add(data.getClipData().getItemAt(i).getUri());
+ }
+ }
+ cb.second.onReceiveValue(dataUris.toArray(new Uri[0]));
+ }
+ }
}
}
@@ -689,7 +689,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
Cid webxdcCid = message.getFileParams().getCids().get(0);
- WebxdcPage webxdc = new WebxdcPage(webxdcCid, message, activity.xmppConnectionService);
+ WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
displayTextMessage(viewHolder, message, darkBackground, type);
viewHolder.image.setVisibility(View.GONE);
viewHolder.audioPlayer.setVisibility(View.GONE);
@@ -572,7 +572,9 @@ public final class MimeUtils {
}
// sometimes this works (as with the commit content api)
if (mimeType == null) {
- mimeType = uri.getQueryParameter("mimeType");
+ try {
+ mimeType = uri.getQueryParameter("mimeType");
+ } catch (final Throwable throwable) { }
}
return mimeType;
}