1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-11 20:49:25 +01:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java
2024-01-10 19:01:48 +01:00

274 lines
11 KiB
Java

/* Copyright (C) 2016-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, José Rebelo, Taavi Eomäe
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.MutableContextWrapper;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBChromeClient;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBWebClient;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface;
public class WebViewSingleton {
private static final Logger LOG = LoggerFactory.getLogger(WebViewSingleton.class);
private static final WebViewSingleton instance = new WebViewSingleton();
private WebView webView = null;
private MutableContextWrapper contextWrapper;
private Looper mainLooper;
private UUID currentRunningUUID;
private Messenger internetHelper = null;
public boolean internetHelperBound;
private boolean internetHelperInstalled;
private CountDownLatch latch;
private WebResourceResponse internetResponse;
private Messenger internetHelperListener;
private WebViewSingleton() {
}
//Internet helper outgoing connection
private final ServiceConnection internetHelperConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
LOG.info("internet helper service bound");
internetHelperBound = true;
internetHelper = new Messenger(service);
}
public void onServiceDisconnected(ComponentName className) {
LOG.info("internet helper service unbound");
internetHelper = null;
internetHelperBound = false;
}
};
public static synchronized void ensureCreated(Activity context) {
if (instance.webView == null) {
instance.contextWrapper = new MutableContextWrapper(context);
instance.mainLooper = context.getMainLooper();
instance.webView = new WebView(instance.contextWrapper);
WebView.setWebContentsDebuggingEnabled(true);
instance.webView.setWillNotDraw(true);
instance.webView.clearCache(true);
instance.webView.setWebViewClient(new GBWebClient());
instance.webView.setWebChromeClient(new GBChromeClient());
WebSettings webSettings = instance.webView.getSettings();
//noinspection SetJavaScriptEnabled
webSettings.setJavaScriptEnabled(true);
//needed to access the DOM
webSettings.setDomStorageEnabled(true);
//needed for localstorage
webSettings.setDatabaseEnabled(true);
// #3373 #3424 - Fix configuration for pebble apps
// TODO: this should use a WebViewAssetLoader
webSettings.setAllowFileAccess(true);
}
}
public static WebViewSingleton getInstance() {
return instance;
}
public WebResourceResponse send(Message webRequest) throws RemoteException, InterruptedException {
webRequest.replyTo = internetHelperListener;
latch = new CountDownLatch(1); //the messenger should run on a single thread, hence we don't need to be worried about concurrency. This approach however is certainly not ideal.
internetHelper.send(webRequest);
latch.await();
return internetResponse;
}
//Internet helper inbound (responses) handler
private class IncomingHandler extends Handler {
private String getCharsetFromHeaders(String contentType) {
if (contentType != null && contentType.toLowerCase().trim().contains("charset=")) {
String[] parts = contentType.toLowerCase().trim().split("=");
if (parts.length > 0)
return parts[1];
}
return null;
}
@Override
public void handleMessage(Message msg) {
Bundle data = msg.getData();
LOG.debug("WEBVIEW: internet helper returned: " + data.getString("response"));
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
internetResponse = new WebResourceResponse(data.getString("content-type"), data.getString("content-encoding"), 200, "OK",
headers,
new ByteArrayInputStream(data.getString("response").getBytes(Charset.forName(getCharsetFromHeaders(data.getString("content-type")))))
);
latch.countDown();
}
}
@NonNull
public WebView getWebView(Context context) {
contextWrapper.setBaseContext(context);
return webView;
}
/**
* Checks that the webview is up and the given app is running.
* @param uuid the uuid of the application expected to be running
* @throws IllegalStateException when the webview is not active or the app is not running
*/
public void checkAppRunning(@NonNull UUID uuid) {
if (webView == null) {
throw new IllegalStateException("instance.webView is null!");
}
if (!uuid.equals(currentRunningUUID)) {
throw new IllegalStateException("Expected app " + uuid + " is not running, but " + currentRunningUUID + " is.");
}
}
public void runJavascriptInterface(@NonNull Activity context, @NonNull GBDevice device, @NonNull UUID uuid) {
ensureCreated(context);
runJavascriptInterface(device, uuid);
}
public void runJavascriptInterface(@NonNull GBDevice device, @NonNull UUID uuid) {
if (uuid.equals(currentRunningUUID)) {
LOG.debug("WEBVIEW uuid not changed keeping the old context");
} else {
final JSInterface jsInterface = new JSInterface(device, uuid);
LOG.debug("WEBVIEW uuid changed, restarting");
currentRunningUUID = uuid;
invokeWebview(new WebViewRunnable() {
@Override
public void invoke(WebView webView) {
webView.onResume();
webView.removeJavascriptInterface("GBjs");
webView.addJavascriptInterface(jsInterface, "GBjs");
webView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
}
});
if (contextWrapper != null && !internetHelperBound && !internetHelperInstalled) {
String internetHelperPkg = "nodomain.freeyourgadget.internethelper";
String internetHelperCls = internetHelperPkg + ".MyService";
try {
contextWrapper.getPackageManager().getApplicationInfo(internetHelperPkg, 0);
Intent intent = new Intent();
intent.setComponent(new ComponentName(internetHelperPkg, internetHelperCls));
contextWrapper.getApplicationContext().bindService(intent, internetHelperConnection, Context.BIND_AUTO_CREATE);
internetHelperListener = new Messenger(new IncomingHandler());
internetHelperInstalled = true;
}
catch (PackageManager.NameNotFoundException e) {
internetHelperInstalled = false;
LOG.info("WEBVIEW: Internet helper not installed, only mimicked HTTP requests will work.");
}
}
}
}
public void stopJavascriptInterface() {
invokeWebview(new WebViewRunnable() {
@Override
public void invoke(WebView webView) {
webView.removeJavascriptInterface("GBjs");
webView.loadUrl("about:blank");
}
});
}
public void disposeWebView() {
if (internetHelperBound) {
LOG.debug("WEBVIEW: will unbind the internet helper");
contextWrapper.getApplicationContext().unbindService(internetHelperConnection);
internetHelperBound = false;
}
currentRunningUUID = null;
invokeWebview(new WebViewRunnable() {
@Override
public void invoke(WebView webView) {
webView.removeJavascriptInterface("GBjs");
// webView.setWebChromeClient(null);
// webView.setWebViewClient(null);
webView.clearHistory();
webView.clearCache(true);
webView.loadUrl("about:blank");
// webView.freeMemory();
webView.pauseTimers();
// webView.destroy();
// webView = null;
// contextWrapper = null;
// jsInterface = null;
}
});
}
public void invokeWebview(final WebViewRunnable runnable) {
if (webView == null || mainLooper == null) {
LOG.warn("Webview already disposed, ignoring runnable");
return;
}
new Handler(mainLooper).post(new Runnable() {
@Override
public void run() {
if (webView == null) {
LOG.warn("Webview already disposed, cannot invoke runnable");
return;
}
runnable.invoke(webView);
}
});
}
public interface WebViewRunnable {
/**
* Called in the main thread with a non-null webView webView
* @param webView the webview, never null
*/
void invoke(WebView webView);
}
}