mirror of
https://github.com/deltachat/deltachat-android.git
synced 2025-10-03 09:49:21 +02:00
638 lines
25 KiB
Java
638 lines
25 KiB
Java
package org.thoughtcrime.securesms;
|
||
|
||
import android.app.Activity;
|
||
import android.content.Context;
|
||
import android.content.Intent;
|
||
import android.content.res.Configuration;
|
||
import android.graphics.Bitmap;
|
||
import android.graphics.drawable.BitmapDrawable;
|
||
import android.graphics.drawable.Drawable;
|
||
import android.net.Uri;
|
||
import android.os.Bundle;
|
||
import android.speech.tts.TextToSpeech;
|
||
import android.text.TextUtils;
|
||
import android.util.Base64;
|
||
import android.util.Log;
|
||
import android.view.Menu;
|
||
import android.view.MenuItem;
|
||
import android.view.View;
|
||
import android.webkit.JavascriptInterface;
|
||
import android.webkit.MimeTypeMap;
|
||
import android.webkit.ValueCallback;
|
||
import android.webkit.WebChromeClient;
|
||
import android.webkit.WebResourceResponse;
|
||
import android.webkit.WebSettings;
|
||
import android.webkit.WebView;
|
||
import android.widget.Toast;
|
||
|
||
import androidx.annotation.NonNull;
|
||
import androidx.annotation.RequiresApi;
|
||
import androidx.appcompat.app.ActionBar;
|
||
import androidx.core.app.TaskStackBuilder;
|
||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||
import androidx.core.graphics.drawable.IconCompat;
|
||
|
||
import com.b44t.messenger.DcChat;
|
||
import com.b44t.messenger.DcContext;
|
||
import com.b44t.messenger.DcEvent;
|
||
import com.b44t.messenger.DcMsg;
|
||
import com.b44t.messenger.rpc.Rpc;
|
||
import com.b44t.messenger.rpc.RpcException;
|
||
import com.google.common.base.Charsets;
|
||
|
||
import org.json.JSONObject;
|
||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||
import org.thoughtcrime.securesms.util.Prefs;
|
||
import org.thoughtcrime.securesms.util.Util;
|
||
|
||
import java.io.ByteArrayInputStream;
|
||
import java.io.FileOutputStream;
|
||
import java.io.IOException;
|
||
import java.io.InputStream;
|
||
import java.io.UnsupportedEncodingException;
|
||
import java.net.URLEncoder;
|
||
import java.util.Arrays;
|
||
import java.util.HashMap;
|
||
import java.util.Locale;
|
||
import java.util.Map;
|
||
|
||
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
|
||
private static final String TAG = WebxdcActivity.class.getSimpleName();
|
||
private static final String EXTRA_ACCOUNT_ID = "accountId";
|
||
private static final String EXTRA_APP_MSG_ID = "appMessageId";
|
||
private static final String EXTRA_HIDE_ACTION_BAR = "hideActionBar";
|
||
private static final String EXTRA_HREF = "href";
|
||
private static final int REQUEST_CODE_FILE_PICKER = 51426;
|
||
private static long lastOpenTime = 0;
|
||
|
||
private ValueCallback<Uri[]> filePathCallback;
|
||
private DcContext dcContext;
|
||
private Rpc rpc;
|
||
private DcMsg dcAppMsg;
|
||
private String baseURL;
|
||
private String sourceCodeUrl = "";
|
||
private String selfAddr;
|
||
private int sendUpdateMaxSize;
|
||
private int sendUpdateInterval;
|
||
private boolean internetAccess = false;
|
||
private boolean hideActionBar = false;
|
||
|
||
private TextToSpeech tts;
|
||
|
||
public static void openMaps(Context context, int chatId) {
|
||
DcContext dcContext = DcHelper.getContext(context);
|
||
int msgId = dcContext.initWebxdcIntegration(chatId);
|
||
if (msgId == 0) {
|
||
try {
|
||
InputStream inputStream = context.getResources().getAssets().open("webxdc/maps.xdc");
|
||
String outputFile = DcHelper.getBlobdirFile(dcContext, "maps", ".xdc");
|
||
Util.copy(inputStream, new FileOutputStream(outputFile));
|
||
dcContext.setWebxdcIntegration(outputFile);
|
||
msgId = dcContext.initWebxdcIntegration(chatId);
|
||
} catch (IOException e) {
|
||
e.printStackTrace();
|
||
}
|
||
if (msgId == 0) {
|
||
Toast.makeText(context, "Cannot get maps.xdc, see log for details.", Toast.LENGTH_LONG).show();
|
||
return;
|
||
}
|
||
}
|
||
openWebxdcActivity(context, msgId, true, "");
|
||
}
|
||
|
||
public static void openWebxdcActivity(Context context, DcMsg instance) {
|
||
openWebxdcActivity(context, instance, "");
|
||
}
|
||
|
||
public static void openWebxdcActivity(Context context, @NonNull DcMsg instance, String href) {
|
||
openWebxdcActivity(context, instance.getId(), false, href);
|
||
}
|
||
|
||
public static void openWebxdcActivity(Context context, int msgId, boolean hideActionBar, String href) {
|
||
if (!Util.isClickedRecently()) {
|
||
if (Prefs.isDeveloperModeEnabled(context)) {
|
||
WebView.setWebContentsDebuggingEnabled(true);
|
||
}
|
||
context.startActivity(getWebxdcIntent(context, msgId, hideActionBar, href));
|
||
}
|
||
}
|
||
|
||
private static Intent getWebxdcIntent(Context context, int msgId, boolean hideActionBar, String href) {
|
||
DcContext dcContext = DcHelper.getContext(context);
|
||
Intent intent = new Intent(context, WebxdcActivity.class);
|
||
intent.setAction(Intent.ACTION_VIEW);
|
||
intent.putExtra(EXTRA_ACCOUNT_ID, dcContext.getAccountId());
|
||
intent.putExtra(EXTRA_APP_MSG_ID, msgId);
|
||
intent.putExtra(EXTRA_HIDE_ACTION_BAR, hideActionBar);
|
||
intent.putExtra(EXTRA_HREF, href);
|
||
return intent;
|
||
}
|
||
|
||
private static Intent[] getWebxdcIntentWithParentStack(Context context, int msgId) {
|
||
DcContext dcContext = DcHelper.getContext(context);
|
||
|
||
final Intent chatIntent = new Intent(context, ConversationActivity.class)
|
||
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
|
||
.setAction(Intent.ACTION_VIEW);
|
||
|
||
final Intent webxdcIntent = getWebxdcIntent(context, msgId, false, "");
|
||
|
||
return TaskStackBuilder.create(context)
|
||
.addNextIntentWithParentStack(chatIntent)
|
||
.addNextIntent(webxdcIntent)
|
||
.getIntents();
|
||
}
|
||
|
||
@Override
|
||
protected void onCreate(Bundle state, boolean ready) {
|
||
super.onCreate(state, ready);
|
||
rpc = DcHelper.getRpc(this);
|
||
initTTS();
|
||
|
||
Bundle b = getIntent().getExtras();
|
||
hideActionBar = b.getBoolean(EXTRA_HIDE_ACTION_BAR, false);
|
||
|
||
// enter fullscreen mode if necessary,
|
||
// this is needed here because if the app is opened while already in landscape mode, onConfigurationChanged() is not triggered
|
||
setScreenMode(getResources().getConfiguration());
|
||
|
||
webView.setWebChromeClient(new WebChromeClient() {
|
||
@Override
|
||
@RequiresApi(21)
|
||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
|
||
if (WebxdcActivity.this.filePathCallback != null) {
|
||
WebxdcActivity.this.filePathCallback.onReceiveValue(null);
|
||
}
|
||
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);
|
||
WebxdcActivity.this.startActivityForResult(Intent.createChooser(intent, getString(R.string.select)), REQUEST_CODE_FILE_PICKER);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
DcEventCenter eventCenter = DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
|
||
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
|
||
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
|
||
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
|
||
|
||
int appMessageId = b.getInt(EXTRA_APP_MSG_ID);
|
||
int accountId = b.getInt(EXTRA_ACCOUNT_ID);
|
||
this.dcContext = DcHelper.getContext(getApplicationContext());
|
||
if (accountId != dcContext.getAccountId()) {
|
||
AccountManager.getInstance().switchAccount(getApplicationContext(), accountId);
|
||
this.dcContext = DcHelper.getContext(getApplicationContext());
|
||
}
|
||
|
||
this.dcAppMsg = this.dcContext.getMsg(appMessageId);
|
||
if (!this.dcAppMsg.isOk()) {
|
||
Toast.makeText(this, "Webxdc does no longer exist.", Toast.LENGTH_LONG).show();
|
||
finish();
|
||
return;
|
||
}
|
||
|
||
// `msg_id` in the subdomain makes sure, different apps using same files do not share the same cache entry
|
||
// (WebView may use a global cache shared across objects).
|
||
// (a random-id would also work, but would need maintenance and does not add benefits as we regard the file-part interceptRequest() only,
|
||
// also a random-id is not that useful for debugging)
|
||
this.baseURL = "https://acc" + dcContext.getAccountId() + "-msg" + appMessageId + ".localhost";
|
||
|
||
final JSONObject info = this.dcAppMsg.getWebxdcInfo();
|
||
internetAccess = JsonUtils.optBoolean(info, "internet_access");
|
||
selfAddr = info.optString("self_addr");
|
||
sendUpdateMaxSize = info.optInt("send_update_max_size");
|
||
sendUpdateInterval = info.optInt("send_update_interval");
|
||
|
||
toggleFakeProxy(!internetAccess);
|
||
|
||
WebSettings webSettings = webView.getSettings();
|
||
webSettings.setJavaScriptEnabled(true);
|
||
webSettings.setAllowFileAccess(false);
|
||
webSettings.setBlockNetworkLoads(!internetAccess);
|
||
webSettings.setAllowContentAccess(false);
|
||
webSettings.setGeolocationEnabled(false);
|
||
webSettings.setAllowFileAccessFromFileURLs(false);
|
||
webSettings.setAllowUniversalAccessFromFileURLs(false);
|
||
webSettings.setDatabaseEnabled(true);
|
||
webSettings.setDomStorageEnabled(true);
|
||
webView.setNetworkAvailable(internetAccess); // this does not block network but sets `window.navigator.isOnline` in js land
|
||
webView.addJavascriptInterface(new InternalJSApi(), "InternalJSApi");
|
||
|
||
String extraHref = b.getString(EXTRA_HREF, "");
|
||
if (TextUtils.isEmpty(extraHref)) {
|
||
extraHref = "index.html";
|
||
}
|
||
|
||
String href = baseURL + "/" + extraHref;
|
||
String encodedHref = "";
|
||
try {
|
||
encodedHref = URLEncoder.encode(href, Charsets.UTF_8.name());
|
||
} catch (UnsupportedEncodingException e) {
|
||
e.printStackTrace();
|
||
}
|
||
|
||
long timeDelta = System.currentTimeMillis() - lastOpenTime;
|
||
final String url = this.baseURL + "/webxdc_bootstrap324567869.html?i=" + (internetAccess? "1" : "0") + "&href=" + encodedHref;
|
||
Util.runOnAnyBackgroundThread(() -> {
|
||
if (timeDelta < 2000) {
|
||
// this is to avoid getting stuck in the FILL500 in some devices if the
|
||
// previous webview was not destroyed yet and a new app is opened too soon
|
||
Util.sleep(1000);
|
||
}
|
||
Util.runOnMain(() -> webView.loadUrl(url));
|
||
});
|
||
|
||
Util.runOnAnyBackgroundThread(() -> {
|
||
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
|
||
Util.runOnMain(() -> {
|
||
updateTitleAndMenu(info, chat);
|
||
});
|
||
});
|
||
}
|
||
|
||
@Override
|
||
public void onResume() {
|
||
super.onResume();
|
||
DcHelper.getNotificationCenter(this).updateVisibleWebxdc(dcContext.getAccountId(), dcAppMsg.getId());
|
||
}
|
||
|
||
@Override
|
||
protected void onPause() {
|
||
super.onPause();
|
||
DcHelper.getNotificationCenter(this).clearVisibleWebxdc();
|
||
}
|
||
|
||
@Override
|
||
protected void onDestroy() {
|
||
lastOpenTime = System.currentTimeMillis();
|
||
DcHelper.getEventCenter(this.getApplicationContext()).removeObservers(this);
|
||
leaveRealtimeChannel();
|
||
tts.shutdown();
|
||
super.onDestroy();
|
||
}
|
||
|
||
@Override
|
||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||
// do not call super.onPrepareOptionsMenu() as the default "Search" menu is not needed
|
||
menu.clear();
|
||
this.getMenuInflater().inflate(R.menu.webxdc, menu);
|
||
menu.findItem(R.id.source_code).setVisible(!sourceCodeUrl.isEmpty());
|
||
return true;
|
||
}
|
||
|
||
@Override
|
||
public boolean onOptionsItemSelected(MenuItem item) {
|
||
super.onOptionsItemSelected(item);
|
||
int itemId = item.getItemId();
|
||
if (itemId == R.id.menu_add_to_home_screen) {
|
||
addToHomeScreen(this, dcAppMsg.getId());
|
||
return true;
|
||
} else if (itemId == R.id.source_code) {
|
||
IntentUtils.showInBrowser(this, sourceCodeUrl);
|
||
return true;
|
||
} else if (itemId == R.id.show_in_chat) {
|
||
showInChat();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
@Override
|
||
public void onConfigurationChanged(Configuration newConfig) {
|
||
Log.i(TAG, "onConfigurationChanged(" + newConfig.orientation + ")");
|
||
super.onConfigurationChanged(newConfig);
|
||
// orientation might have changed, enter/exit fullscreen mode if needed
|
||
setScreenMode(newConfig);
|
||
}
|
||
|
||
private void initTTS() {
|
||
tts = new TextToSpeech(this, new TextToSpeech.OnInitListener() {
|
||
@Override
|
||
public void onInit(int status) {
|
||
Log.i(TAG, "TTS Init Status: " + status);
|
||
}
|
||
});
|
||
}
|
||
private void setScreenMode(Configuration config) {
|
||
// enter/exit fullscreen mode depending on orientation (landscape/portrait),
|
||
// on tablets there is enough height so fullscreen mode is never enabled there
|
||
boolean enable = config.orientation == Configuration.ORIENTATION_LANDSCAPE && !getResources().getBoolean(R.bool.isBigScreen);
|
||
getWindow().getDecorView().setSystemUiVisibility(enable? View.SYSTEM_UI_FLAG_FULLSCREEN : 0);
|
||
ActionBar actionBar = getSupportActionBar();
|
||
if (actionBar != null) {
|
||
if (hideActionBar || enable) {
|
||
actionBar.hide();
|
||
} else {
|
||
actionBar.show();
|
||
}
|
||
}
|
||
}
|
||
|
||
// This is usually only called when internetAccess == true or for mailto/openpgp4fpr scheme,
|
||
// because when internetAccess == false, the page is loaded inside an iframe,
|
||
// and WebViewClient.shouldOverrideUrlLoading is not called for HTTP(S) links inside the iframe
|
||
@Override
|
||
protected boolean openOnlineUrl(String url) {
|
||
Log.i(TAG, "openOnlineUrl: " + url);
|
||
if (url.startsWith("mailto:") || url.startsWith("openpgp4fpr:")) {
|
||
return super.openOnlineUrl(url);
|
||
}
|
||
return !internetAccess; // returning `false` continues loading in WebView; returning `true` let WebView abort loading
|
||
}
|
||
|
||
@Override
|
||
protected WebResourceResponse interceptRequest(String rawUrl) {
|
||
Log.i(TAG, "interceptRequest: " + rawUrl);
|
||
WebResourceResponse res = null;
|
||
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);
|
||
res = new WebResourceResponse("text/javascript", "UTF-8", targetStream);
|
||
} else if (path.equalsIgnoreCase("/webxdc_bootstrap324567869.html")) {
|
||
InputStream targetStream = getResources().openRawResource(R.raw.webxdc_wrapper);
|
||
res = new WebResourceResponse("text/html", "UTF-8", targetStream);
|
||
} else if (path.equalsIgnoreCase("/sandboxed_iframe_rtcpeerconnection_check_5965668501706.html")) {
|
||
InputStream targetStream = getResources().openRawResource(R.raw.sandboxed_iframe_rtcpeerconnection_check);
|
||
res = new WebResourceResponse("text/html", "UTF-8", targetStream);
|
||
} else {
|
||
byte[] blob = this.dcAppMsg.getWebxdcBlob(path);
|
||
if (blob == null) {
|
||
if (internetAccess) {
|
||
return null; // do not intercept request
|
||
}
|
||
throw new Exception("\"" + path + "\" not found");
|
||
}
|
||
String ext = MediaUtil.getFileExtensionFromUrl(path);
|
||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
|
||
if (mimeType == null) {
|
||
switch (ext) {
|
||
case "js": mimeType = "text/javascript"; break;
|
||
case "wasm": mimeType = "application/wasm"; 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);
|
||
res = new WebResourceResponse(mimeType, encoding, targetStream);
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
InputStream targetStream = new ByteArrayInputStream(("Webxdc Request Error: " + e.getMessage()).getBytes());
|
||
res = new WebResourceResponse("text/plain", "UTF-8", targetStream);
|
||
}
|
||
|
||
if (!internetAccess) {
|
||
Map<String, String> headers = new HashMap<>();
|
||
headers.put("Content-Security-Policy",
|
||
"default-src 'self'; "
|
||
+ "style-src 'self' 'unsafe-inline' blob: ; "
|
||
+ "font-src 'self' data: blob: ; "
|
||
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: ; "
|
||
+ "connect-src 'self' data: blob: ; "
|
||
+ "img-src 'self' data: blob: ; "
|
||
+ "media-src 'self' data: blob: ;"
|
||
+ "webrtc 'block' ; "
|
||
);
|
||
headers.put("X-DNS-Prefetch-Control", "off");
|
||
res.setResponseHeaders(headers);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
private void callJavaScriptFunction(String func) {
|
||
webView.evaluateJavascript("document.getElementById('frame').contentWindow." + func + ";", null);
|
||
}
|
||
|
||
@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, "handling status update event");
|
||
callJavaScriptFunction("__webxdcUpdate()");
|
||
} else if ((eventId == DcContext.DC_EVENT_WEBXDC_REALTIME_DATA && event.getData1Int() == dcAppMsg.getId())) {
|
||
Log.i(TAG, "handling realtime data event");
|
||
StringBuilder data = new StringBuilder();
|
||
for (byte b : event.getData2Blob()) {
|
||
data.append(((int) b) + ",");
|
||
}
|
||
callJavaScriptFunction("__webxdcRealtimeData([" + data + "])");
|
||
} else if ((eventId == DcContext.DC_EVENT_MSGS_CHANGED && event.getData2Int() == dcAppMsg.getId())) {
|
||
this.dcAppMsg = this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db
|
||
Util.runOnAnyBackgroundThread(() -> {
|
||
final JSONObject info = dcAppMsg.getWebxdcInfo();
|
||
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
|
||
Util.runOnMain(() -> {
|
||
updateTitleAndMenu(info, chat);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
private void updateTitleAndMenu(JSONObject info, DcChat chat) {
|
||
final String docName = JsonUtils.optString(info, "document");
|
||
final String xdcName = JsonUtils.optString(info, "name");
|
||
final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url");
|
||
getSupportActionBar().setTitle((docName.isEmpty() ? xdcName : docName) + " – " + chat.getName());
|
||
if (!sourceCodeUrl.equals(currSourceCodeUrl)) {
|
||
sourceCodeUrl = currSourceCodeUrl;
|
||
invalidateOptionsMenu();
|
||
}
|
||
}
|
||
|
||
private void showInChat() {
|
||
Intent intent = new Intent(this, ConversationActivity.class);
|
||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcAppMsg.getChatId());
|
||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcAppMsg, dcContext));
|
||
startActivity(intent);
|
||
}
|
||
|
||
public static void addToHomeScreen(Activity activity, int msgId) {
|
||
Context context = activity.getApplicationContext();
|
||
try {
|
||
DcContext dcContext = DcHelper.getContext(context);
|
||
DcMsg msg = dcContext.getMsg(msgId);
|
||
final JSONObject info = msg.getWebxdcInfo();
|
||
|
||
final String docName = JsonUtils.optString(info, "document");
|
||
final String xdcName = JsonUtils.optString(info, "name");
|
||
byte[] blob = msg.getWebxdcBlob(JsonUtils.optString(info, "icon"));
|
||
ByteArrayInputStream is = new ByteArrayInputStream(blob);
|
||
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
|
||
Bitmap bitmap = drawable.getBitmap();
|
||
|
||
ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId)
|
||
.setShortLabel(docName.isEmpty() ? xdcName : docName)
|
||
.setIcon(IconCompat.createWithBitmap(bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too small circle and defamiliarize the icon too much
|
||
.setIntents(getWebxdcIntentWithParentStack(context, msgId))
|
||
.build();
|
||
|
||
Toast.makeText(context, R.string.one_moment, Toast.LENGTH_SHORT).show();
|
||
if (!ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) {
|
||
Toast.makeText(context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG).show();
|
||
}
|
||
} catch(Exception e) {
|
||
Toast.makeText(context, "ErrAddToHomescreen: " + e, Toast.LENGTH_LONG).show();
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||
if (reqCode == REQUEST_CODE_FILE_PICKER && filePathCallback != null) {
|
||
Uri[] dataUris = null;
|
||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||
try {
|
||
if (data.getDataString() != null) {
|
||
dataUris = new Uri[]{Uri.parse(data.getDataString())};
|
||
} else if (data.getClipData() != null) {
|
||
final int numSelectedFiles = data.getClipData().getItemCount();
|
||
dataUris = new Uri[numSelectedFiles];
|
||
for (int i = 0; i < numSelectedFiles; i++) {
|
||
dataUris[i] = data.getClipData().getItemAt(i).getUri();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
filePathCallback.onReceiveValue(dataUris);
|
||
filePathCallback = null;
|
||
}
|
||
super.onActivityResult(reqCode, resultCode, data);
|
||
}
|
||
|
||
private void leaveRealtimeChannel() {
|
||
int accountId = dcContext.getAccountId();
|
||
int msgId = dcAppMsg.getId();
|
||
try {
|
||
rpc.leaveWebxdcRealtime(accountId, msgId);
|
||
} catch (RpcException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
class InternalJSApi {
|
||
@JavascriptInterface
|
||
public int sendUpdateMaxSize() {
|
||
return WebxdcActivity.this.sendUpdateMaxSize;
|
||
}
|
||
|
||
@JavascriptInterface
|
||
public int sendUpdateInterval() {
|
||
return WebxdcActivity.this.sendUpdateInterval;
|
||
}
|
||
|
||
@JavascriptInterface
|
||
public String selfAddr() {
|
||
return WebxdcActivity.this.selfAddr;
|
||
}
|
||
|
||
/** @noinspection unused*/
|
||
@JavascriptInterface
|
||
public String selfName() {
|
||
return WebxdcActivity.this.dcContext.getName();
|
||
}
|
||
|
||
/** @noinspection unused*/
|
||
@JavascriptInterface
|
||
public boolean sendStatusUpdate(String payload) {
|
||
Log.i(TAG, "sendStatusUpdate");
|
||
if (!WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate(WebxdcActivity.this.dcAppMsg.getId(), payload)) {
|
||
DcChat dcChat = WebxdcActivity.this.dcContext.getChat(WebxdcActivity.this.dcAppMsg.getChatId());
|
||
Toast.makeText(WebxdcActivity.this,
|
||
dcChat.isContactRequest() ?
|
||
WebxdcActivity.this.getString(R.string.accept_request_first) :
|
||
WebxdcActivity.this.dcContext.getLastError(),
|
||
Toast.LENGTH_LONG).show();
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/** @noinspection unused*/
|
||
@JavascriptInterface
|
||
public String getStatusUpdates(int lastKnownSerial) {
|
||
Log.i(TAG, "getStatusUpdates");
|
||
return WebxdcActivity.this.dcContext.getWebxdcStatusUpdates(WebxdcActivity.this.dcAppMsg.getId(), lastKnownSerial );
|
||
}
|
||
|
||
/** @noinspection unused*/
|
||
@JavascriptInterface
|
||
public String sendToChat(String message) {
|
||
Log.i(TAG, "sendToChat");
|
||
try {
|
||
JSONObject jsonObject = new JSONObject(message);
|
||
|
||
String text = null;
|
||
byte[] data = null;
|
||
String name = null;
|
||
if (jsonObject.has("base64")) {
|
||
data = Base64.decode(jsonObject.getString("base64"), Base64.NO_WRAP | Base64.NO_PADDING);
|
||
name = jsonObject.getString("name");
|
||
}
|
||
if (jsonObject.has("text")) {
|
||
text = jsonObject.getString("text");
|
||
}
|
||
|
||
if (TextUtils.isEmpty(text) && TextUtils.isEmpty(name)) {
|
||
return "provided file is invalid, you need to set both name and base64 content";
|
||
}
|
||
|
||
DcHelper.sendToChat(WebxdcActivity.this, data, "application/octet-stream", name, text);
|
||
return null;
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
return e.toString();
|
||
}
|
||
}
|
||
|
||
/** @noinspection unused*/
|
||
@JavascriptInterface
|
||
public void sendRealtimeAdvertisement() {
|
||
int accountId = WebxdcActivity.this.dcContext.getAccountId();
|
||
int msgId = WebxdcActivity.this.dcAppMsg.getId();
|
||
try {
|
||
WebxdcActivity.this.rpc.sendWebxdcRealtimeAdvertisement(accountId, msgId);
|
||
} catch (RpcException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
/** @noinspection unused*/
|
||
@JavascriptInterface
|
||
public void leaveRealtimeChannel() {
|
||
WebxdcActivity.this.leaveRealtimeChannel();
|
||
}
|
||
|
||
/** @noinspection unused*/
|
||
@JavascriptInterface
|
||
public void sendRealtimeData(String jsonData) {
|
||
int accountId = WebxdcActivity.this.dcContext.getAccountId();
|
||
int msgId = WebxdcActivity.this.dcAppMsg.getId();
|
||
try {
|
||
Integer[] data = JsonUtils.fromJson(jsonData, Integer[].class);
|
||
WebxdcActivity.this.rpc.sendWebxdcRealtimeData(accountId, msgId, Arrays.asList(data));
|
||
} catch (IOException | RpcException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
@JavascriptInterface
|
||
public void ttsSpeak(String text, String lang) {
|
||
if (lang != null && !lang.isEmpty()) tts.setLanguage(Locale.forLanguageTag(lang));
|
||
tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
|
||
}
|
||
|
||
}
|
||
}
|