deltachat/src/main/java/org/thoughtcrime/securesms/WebViewActivity.java
2025-08-16 02:10:02 +02:00

288 lines
9.3 KiB
Java

package org.thoughtcrime.securesms;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.SearchView;
import androidx.webkit.ProxyConfig;
import androidx.webkit.ProxyController;
import androidx.webkit.WebSettingsCompat;
import androidx.webkit.WebViewFeature;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.IntentUtils;
public class WebViewActivity extends PassphraseRequiredActionBarActivity
implements SearchView.OnQueryTextListener,
WebView.FindListener
{
private static final String TAG = WebViewActivity.class.getSimpleName();
protected WebView webView;
protected void toggleFakeProxy(boolean enable) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
if (enable) {
// Set proxy to non-routable address.
ProxyConfig proxyConfig = new ProxyConfig.Builder()
.removeImplicitRules()
.addProxyRule("0.0.0.0")
.build();
ProxyController.getInstance().setProxyOverride(proxyConfig, Runnable::run, () -> Log.i(TAG, "Set WebView proxy."));
} else {
ProxyController.getInstance().clearProxyOverride(Runnable::run, () -> Log.i(TAG, "Cleared WebView proxy."));
}
} else {
Log.w(TAG, "Cannot " + (enable? "set": "clear") + " WebView proxy.");
}
}
@Override
protected void onCreate(Bundle state, boolean ready) {
setContentView(R.layout.web_view_activity);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setElevation(0); // TODO: use custom toolbar instead
}
webView = findViewById(R.id.webview);
webView.setWebViewClient(new WebViewClient() {
// IMPORTANT: this is will likely not be called inside iframes.
// `shouldOverrideUrlLoading()` is called when the user clicks a URL,
// returning `true` causes the WebView to abort loading the URL,
// returning `false` causes the WebView to continue loading the URL as usual.
// the method is not called for POST request nor for on-page-links.
//
// nb: from API 24, `shouldOverrideUrlLoading(String)` is deprecated and
// `shouldOverrideUrlLoading(WebResourceRequest)` shall be used.
// the new one has the same functionality, and the old one still exist,
// so, to support all systems, for now, using the old one seems to be the simplest way.
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null) {
String schema = url.split(":")[0].toLowerCase();
switch (schema) {
case "http":
case "https":
case "mailto":
case "openpgp4fpr":
return openOnlineUrl(url);
}
}
// by returning `true`, we also abort loading other URLs in our WebView;
// eg. that might be weird or internal protocols.
// if we come over really useful things, we should allow that explicitly.
return true;
}
@Override
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);
// 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,
// "safe browsing" will never be able to report issues, so it can be disabled.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
webView.getSettings().setSafeBrowsingEnabled(false);
}
}
protected void setForceDark() {
if (Build.VERSION.SDK_INT <= 32 && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
// needed for older API (tested on android7) that do not set `color-scheme` without the following hint
WebSettingsCompat.setForceDark(webView.getSettings(),
DynamicTheme.isDarkTheme(this) ? WebSettingsCompat.FORCE_DARK_ON : WebSettingsCompat.FORCE_DARK_OFF);
}
}
@Override
protected void onPause() {
super.onPause();
webView.onPause();
}
@Override
public void onResume() {
super.onResume();
webView.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
webView.destroy();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
inflater.inflate(R.menu.web_view, menu);
try {
MenuItem searchItem = menu.findItem(R.id.menu_search_web_view);
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(final MenuItem item) {
searchMenu = menu;
WebViewActivity.this.lastQuery = "";
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, true);
return true;
}
@Override
public boolean onMenuItemActionCollapse(final MenuItem item) {
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, false);
return true;
}
});
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
searchView.setQueryHint(getString(R.string.search));
searchView.setIconifiedByDefault(true);
// hide the [X] beside the search field - this is too much noise, search can be aborted eg. by "back"
ImageView closeBtn = searchView.findViewById(R.id.search_close_btn);
if (closeBtn!=null) {
closeBtn.setEnabled(false);
closeBtn.setImageDrawable(null);
}
} catch (Exception e) {
Log.e(TAG, "cannot set up web-view-search: ", e);
}
super.onPrepareOptionsMenu(menu);
return true;
}
// search
private Menu searchMenu = null;
private String lastQuery = "";
private Toast lastToast = null;
private void updateResultCounter(int curr, int total) {
if (searchMenu!=null) {
MenuItem item = searchMenu.findItem(R.id.menu_search_counter);
if (curr!=-1) {
item.setTitle(String.format("%d/%d", total==0? 0 : curr+1, total));
item.setVisible(true);
} else {
item.setVisible(false);
}
}
}
@Override
public boolean onQueryTextSubmit(String query) {
return true; // action handled by listener
}
@Override
public boolean onQueryTextChange(String query) {
String normQuery = query.trim();
lastQuery = normQuery;
if (lastToast!=null) {
lastToast.cancel();
lastToast = null;
}
webView.findAllAsync(normQuery);
return true; // action handled by listener
}
@Override
public void onFindResultReceived (int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting)
{
if (isDoneCounting) {
if (numberOfMatches>0) {
updateResultCounter(activeMatchOrdinal, numberOfMatches);
} else {
if (lastQuery.isEmpty()) {
updateResultCounter(-1, 0); // hide
} else {
String msg = getString(R.string.search_no_result_for_x, lastQuery);
if (lastToast != null) {
lastToast.cancel();
}
lastToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
lastToast.show();
updateResultCounter(0, 0); // show as "0/0"
}
}
}
}
// other actions
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.menu_search_up) {
if (lastQuery.isEmpty()) {
webView.scrollTo(0, 0);
} else {
webView.findNext(false);
}
return true;
} else if (itemId == R.id.menu_search_down) {
if (lastQuery.isEmpty()) {
webView.scrollTo(0, 1000000000);
} else {
webView.findNext(true);
}
return true;
}
return false;
}
// onBackPressed() can be overwritten by derived classes as needed.
// the default behavior (close the activity) is just fine eg. for Webxdc, Connectivity, HTML-mails
protected boolean openOnlineUrl(String url) {
IntentUtils.showInBrowser(this, url);
// returning `true` causes the WebView to abort loading
return true;
}
protected WebResourceResponse interceptRequest(String url) {
return null;
}
}