Replace background WebViewSingleton with proper foreground service

This commit is contained in:
Arjan Schrijver
2025-12-25 14:45:08 +01:00
committed by Arjan Schrijver
parent 2259e6ebb5
commit 775d064262
10 changed files with 248 additions and 277 deletions
+6 -3
View File
@@ -602,6 +602,12 @@
</intent-filter>
</service>
<service
android:name=".service.devices.pebble.webview.PebbleJsService"
android:exported="false"
android:stopWithTask="false"
android:foregroundServiceType="connectedDevice" />
<receiver
android:name=".externalevents.GenericWeatherReceiver"
android:enabled="true"
@@ -885,10 +891,7 @@
<activity
android:name=".activities.ExternalPebbleJSActivity"
android:allowTaskReparenting="true"
android:clearTaskOnLaunch="true"
android:label="@string/app_configure"
android:launchMode="singleTask"
android:parentActivityName=".activities.appmanager.AppManagerActivity"
android:exported="true">
<meta-data
@@ -23,12 +23,9 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -46,7 +43,6 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
@@ -54,7 +50,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient;
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient;
@@ -68,16 +63,12 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
* otherwise it refers to the legacy webview from the activity_legacy_external_pebble_js layout
*/
private WebView myWebView;
public static final String START_BG_WEBVIEW = "start_webview";
public static final String SHOW_CONFIG = "configure";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
boolean showConfig = false;
UUID currentUUID = null;
GBDevice currentDevice = null;
@@ -87,7 +78,7 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
try {
currentUUID = UUID.fromString(confUri.getHost());
} catch (IllegalArgumentException e) {
LOG.error("UUID in incoming configuration is not a valid UUID: " +confUri.toString());
LOG.error("UUID in incoming configuration is not a valid UUID: {}", confUri);
}
//first check if we are still connected to a pebble
@@ -113,71 +104,20 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
}
}
}
showConfig = true; //we are getting incoming configuration data
}
} else {
currentDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
currentUUID = (UUID) extras.getSerializable(DeviceService.EXTRA_APP_UUID);
if (currentDevice != null && extras.getBoolean(START_BG_WEBVIEW, false) && ((PebbleCoordinator) currentDevice.getDeviceCoordinator()).isBackgroundJsEnabled(currentDevice)) {
startBackgroundWebViewAndFinish();
return;
}
showConfig = extras.getBoolean(SHOW_CONFIG, false);
}
if (currentDevice != null && ((PebbleCoordinator) currentDevice.getDeviceCoordinator()).isBackgroundJsEnabled(currentDevice)) {
if (showConfig) {
Objects.requireNonNull(currentDevice, "Must provide a device when invoking this activity");
Objects.requireNonNull(currentUUID, "Must provide a uuid when invoking this activity");
WebViewSingleton.getInstance().runJavascriptInterface(this, currentDevice, currentUUID);
}
// FIXME: is this really supposed to be outside the check for SHOW_CONFIG?
setupBGWebView();
} else {
Objects.requireNonNull(currentDevice, "Must provide a device when invoking this activity without bgjs");
Objects.requireNonNull(currentUUID, "Must provide a uuid when invoking this activity without bgjs");
setupLegacyWebView(currentDevice, currentUUID);
}
Objects.requireNonNull(currentDevice, "Must provide a device when invoking this activity");
Objects.requireNonNull(currentUUID, "Must provide a uuid when invoking this activity");
setupWebView(currentDevice, currentUUID);
}
private void startBackgroundWebViewAndFinish() {
WebViewSingleton.ensureCreated(this, GBWebClient.REQUEST_TYPE_PEBBLE_BACKGROUND_JS);
finish();
}
private void setupBGWebView() {
setContentView(R.layout.activity_external_pebble_js);
myWebView = WebViewSingleton.getInstance().getWebView(this);
if (myWebView.getParent() != null) {
((ViewGroup) myWebView.getParent()).removeView(myWebView);
}
myWebView.setWillNotDraw(false);
myWebView.removeJavascriptInterface("GBActivity");
myWebView.addJavascriptInterface(new ActivityJSInterface(), "GBActivity");
FrameLayout fl = (FrameLayout) findViewById(R.id.webview_placeholder);
fl.addView(myWebView);
myWebView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
@Override
public void onViewDetachedFromWindow(View v) {
v.removeOnAttachStateChangeListener(this);
FrameLayout fl = (FrameLayout) findViewById(R.id.webview_placeholder);
fl.removeAllViews();
}
});
}
private void setupLegacyWebView(@NonNull GBDevice device, @NonNull UUID uuid) {
private void setupWebView(@NonNull GBDevice device, @NonNull UUID uuid) {
setContentView(R.layout.activity_legacy_external_pebble_js);
myWebView = (WebView) findViewById(R.id.configureWebview);
myWebView = findViewById(R.id.configureWebview);
myWebView.clearCache(true);
myWebView.setWebViewClient(new GBWebClient(GBWebClient.REQUEST_TYPE_PEBBLE_APP_CONFIG));
myWebView.setWebChromeClient(new GBChromeClient());
@@ -197,7 +137,7 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
myWebView.addJavascriptInterface(new ActivityJSInterface(), "GBActivity");
myWebView.loadUrl("file:///android_asset/app_config/configure.html");
myWebView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
}
@Override
@@ -206,7 +146,7 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
String queryString = "";
if (confUri != null) {
//getting back with configuration data
LOG.debug("WEBVIEW returned config: " + confUri.toString());
LOG.debug("WEBVIEW returned config: {}", confUri);
try {
queryString = confUri.getEncodedQuery();
} catch (IllegalArgumentException e) {
@@ -501,14 +501,6 @@ public abstract class AbstractAppManagerFragment extends Fragment {
if (requestCode == CHILD_ACTIVITY_WATCHFACE_EDITOR) {
refreshList();
}
if ((mGBDevice.getType() == DeviceType.PEBBLE) && (((PebbleCoordinator) mGBDevice.getDeviceCoordinator()).isBackgroundJsEnabled(mGBDevice))) {
Intent startIntent = new Intent(getContext(), ExternalPebbleJSActivity.class);
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startIntent.putExtra(ExternalPebbleJSActivity.START_BG_WEBVIEW, true);
getContext().startActivity(startIntent);
}
}
protected void sendOrderToDevice(String concatFilename) {
@@ -662,7 +654,6 @@ public abstract class AbstractAppManagerFragment extends Fragment {
final Intent startIntent = new Intent(getContext().getApplicationContext(), ExternalPebbleJSActivity.class);
startIntent.putExtra(DeviceService.EXTRA_APP_UUID, selectedApp.getUUID());
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startIntent.putExtra(ExternalPebbleJSActivity.SHOW_CONFIG, true);
startActivity(startIntent);
return true;
} else if (itemId == R.id.appmanager_app_openinstore) {
@@ -33,6 +33,7 @@ public class GBDeviceEventAppMessage extends GBDeviceEvent {
public UUID appUUID;
public int id;
public String message;
public GBDevice device;
@NonNull
@Override
@@ -42,6 +43,7 @@ public class GBDeviceEventAppMessage extends GBDeviceEvent {
", appUUID=" + appUUID +
", message='" + message + '\'' +
", id=" + id +
", device=" + device +
'}';
}
@@ -24,8 +24,6 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.ParcelUuid;
import android.webkit.ValueCallback;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -48,7 +46,6 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
@@ -63,12 +60,12 @@ import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleInstallable;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble.PebbleLESupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.PebbleJsService;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
class PebbleIoThread extends GBDeviceIoThread {
@@ -115,35 +112,31 @@ class PebbleIoThread extends GBDeviceIoThread {
}
private static void sendAppMessage(GBDeviceEventAppMessage message) {
if (! ((PebbleCoordinator) message.device.getDeviceCoordinator()).isBackgroundJsEnabled(message.device)) {
LOG.info("App message received but Pebble JS Service not running");
return;
}
final String jsEvent;
try {
WebViewSingleton.getInstance().checkAppRunning(message.appUUID);
} catch (IllegalStateException ex) {
LOG.warn("Unable to send app message: " + message, ex);
PebbleJsService.Companion.getInstance().checkAppRunning(message.device, message.appUUID);
} catch (NullPointerException | IllegalStateException ex) {
LOG.warn("Unable to send app message: {}", message, ex);
return;
}
// TODO: handle ACK and NACK types with ids
if (message.type != GBDeviceEventAppMessage.TYPE_APPMESSAGE) {
jsEvent = (GBDeviceEventAppMessage.TYPE_NACK == GBDeviceEventAppMessage.TYPE_APPMESSAGE) ? "NACK" + message.id : "ACK" + message.id;
LOG.debug("WEBVIEW received ACK/NACK:" + message.message + " for uuid: " + message.appUUID + " ID: " + message.id);
LOG.debug("WEBVIEW received ACK/NACK:{} for uuid: {} ID: {}", message.message, message.appUUID, message.id);
} else {
jsEvent = "appmessage";
}
final String appMessage = PebbleUtils.parseIncomingAppMessage(message.message, message.appUUID, message.id);
LOG.debug("to WEBVIEW: event: " + jsEvent + " message: " + appMessage);
WebViewSingleton.getInstance().invokeWebview(new WebViewSingleton.WebViewRunnable() {
@Override
public void invoke(WebView webView) {
webView.evaluateJavascript("if (typeof Pebble == 'object') Pebble.evaluate('" + jsEvent + "',[" + appMessage + "]);", new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
//TODO: the message should be acked here instead of in PebbleIoThread
LOG.debug("Callback from appmessage: " + s);
}
});
}
LOG.debug("to WEBVIEW: event: {} message: {}", jsEvent, appMessage);
PebbleJsService.Companion.getInstance().evaluateJsForDevice(message.device, "if (typeof Pebble == 'object') Pebble.evaluate('" + jsEvent + "',[" + appMessage + "]);", s -> {
//TODO: the message should be acked here instead of in PebbleIoThread
LOG.debug("Callback from appmessage: {}", s);
});
}
@@ -219,11 +212,8 @@ class PebbleIoThread extends GBDeviceIoThread {
}
}
if (((PebbleCoordinator) gbDevice.getDeviceCoordinator()).isBackgroundJsEnabled(gbDevice)) {
Intent startIntent = new Intent(getContext(), ExternalPebbleJSActivity.class);
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice);
startIntent.putExtra(ExternalPebbleJSActivity.START_BG_WEBVIEW, true);
getContext().startActivity(startIntent);
PebbleJsService.Companion.startService(getContext());
PebbleUtils.startJsEngineForDevice(getContext(), gbDevice, new UUID(0, 0));
} else {
LOG.debug("Not enabling background Webview, is disabled in preferences.");
}
@@ -414,7 +404,7 @@ class PebbleIoThread extends GBDeviceIoThread {
}
if (((PebbleCoordinator) gbDevice.getDeviceCoordinator()).isBackgroundJsEnabled(gbDevice)) {
WebViewSingleton.getInstance().disposeWebView();
PebbleUtils.stopJsEngineForDevice(gbDevice);
}
gbDevice.sendDeviceUpdateIntent(getContext());
@@ -547,9 +537,9 @@ class PebbleIoThread extends GBDeviceIoThread {
LOG.info("got GBDeviceEventAppManagement START event for uuid: {}", appMgmt.uuid);
if (((PebbleCoordinator) gbDevice.getDeviceCoordinator()).isBackgroundJsEnabled(gbDevice)) {
if (mPebbleProtocol.hasAppMessageHandler(appMgmt.uuid)) {
WebViewSingleton.getInstance().stopJavascriptInterface();
PebbleUtils.stopJsEngineForDevice(gbDevice);
} else {
WebViewSingleton.getInstance().runJavascriptInterface(getContext(), gbDevice, appMgmt.uuid);
PebbleUtils.startJsEngineForDevice(getContext(), gbDevice, appMgmt.uuid);
}
}
@@ -1782,6 +1782,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
appMessage.appUUID = uuid;
appMessage.id = last_id & 0xff;
appMessage.message = jsonArray.toString();
appMessage.device = getDevice();
return new GBDeviceEvent[]{appMessage, sendBytesAck};
}
@@ -2562,6 +2563,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
}
evtAppMessage.id = idLookup[last_id & 0xff];
evtAppMessage.appUUID = currentRunningApp;
evtAppMessage.device = getDevice();
}
devEvts = new GBDeviceEvent[]{evtAppMessage};
break;
@@ -0,0 +1,170 @@
/* Copyright (C) 2025 Arjan Schrijver
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.service.devices.pebble.webview
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.View
import android.webkit.ValueCallback
import android.webkit.WebView
import androidx.core.app.NotificationCompat
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.util.GB
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
class PebbleJsService : Service() {
companion object {
@Volatile
private var instance: PebbleJsService? = null
fun getInstance(): PebbleJsService? = instance
fun startService(context: Context) {
val intent = Intent(context, PebbleJsService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
val LOG: Logger = LoggerFactory.getLogger(PebbleJsService::class.java)
private val webviews = ConcurrentHashMap<String, WebView>()
private val currentRunningUUID = ConcurrentHashMap<String, UUID>()
override fun onCreate() {
super.onCreate()
instance = this
startForegroundServiceNotification()
}
override fun onDestroy() {
instance = null
destroyAllWebViews()
super.onDestroy()
}
override fun onBind(intent: Intent?) = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
private fun startForegroundServiceNotification() {
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
nm.createNotificationChannel(
NotificationChannel(GB.NOTIFICATION_CHANNEL_ID_PEBBLE_JS, "Pebble JS service", NotificationManager.IMPORTANCE_LOW)
)
}
val notification = NotificationCompat.Builder(this, GB.NOTIFICATION_CHANNEL_ID_PEBBLE_JS)
.setContentTitle("Pebble JavaScript service")
.setContentText("Running Pebble app/watchface JavaScript")
.setSmallIcon(R.drawable.ic_play)
.build()
startForeground(1005, notification)
}
fun startJsForDevice(device: GBDevice, uuid: UUID) {
Handler(Looper.getMainLooper()).post {
if (!webviews.containsKey(device.address)) {
LOG.info("WEBVIEW starting for device ${device.address}")
WebView.setWebContentsDebuggingEnabled(true)
val uiContext = applicationContext.createConfigurationContext(resources.configuration)
val wv = WebView(uiContext).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.databaseEnabled = true
settings.allowContentAccess = true
settings.allowFileAccess = true
settings.allowFileAccessFromFileURLs = true
settings.allowUniversalAccessFromFileURLs = true
visibility = View.GONE
webViewClient = GBWebClient(GBWebClient.REQUEST_TYPE_PEBBLE_BACKGROUND_JS)
webChromeClient = GBChromeClient()
}
wv.setWillNotDraw(true)
wv.clearCache(true)
wv.resumeTimers()
webviews[device.address] = wv
}
if (uuid == currentRunningUUID) {
LOG.debug("WEBVIEW uuid not changed keeping the old context")
} else {
val jsInterface = JSInterface(device, uuid)
LOG.debug("WEBVIEW uuid changed, restarting")
currentRunningUUID[device.address] = uuid
webviews[device.address]?.onResume()
webviews[device.address]?.removeJavascriptInterface("GBjs")
webviews[device.address]?.addJavascriptInterface(jsInterface, "GBjs")
webviews[device.address]?.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500)
}
}
}
fun evaluateJsForDevice(device: GBDevice, js: String, resultCallback: ValueCallback<String>?) {
// The webviews can live in the service, but interaction with them must always be performed from the main thread
Handler(Looper.getMainLooper()).post {
val wv = webviews[device.address]
wv?.evaluateJavascript(js, resultCallback)
}
}
fun stopJsForDevice(device: GBDevice) {
Handler(Looper.getMainLooper()).post {
webviews.remove(device.address)?.destroy()
if (webviews.isEmpty()) {
LOG.info("Last webview stopped, stopping Pebble JS Service...")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
stopSelf()
}
}
}
private fun destroyAllWebViews() {
Handler(Looper.getMainLooper()).post {
webviews.values.forEach { it.destroy() }
webviews.clear()
}
}
fun checkAppRunning(device: GBDevice, appUUID: UUID): Boolean {
return currentRunningUUID[device.address]?.equals(appUUID) ?: false
}
}
@@ -81,6 +81,7 @@ public class GB {
public static final String NOTIFICATION_CHANNEL_ID_LOW_BATTERY = "low_battery";
public static final String NOTIFICATION_CHANNEL_ID_FULL_BATTERY = "full_battery";
public static final String NOTIFICATION_CHANNEL_ID_GPS = "gps";
public static final String NOTIFICATION_CHANNEL_ID_PEBBLE_JS = "pebble_js";
public static final int NOTIFICATION_ID = 1;
public static final int NOTIFICATION_ID_INSTALL = 2;
@@ -91,6 +92,7 @@ public class GB {
public static final int NOTIFICATION_ID_GPS = 7;
public static final int NOTIFICATION_ID_SCAN = 8;
public static final int NOTIFICATION_ID_FULL_BATTERY = 9;
public static final int NOTIFICATION_ID_PEBBLE_JS = 10;
public static final int NOTIFICATION_ID_ERROR = 42;
private static final Logger LOG = LoggerFactory.getLogger(GB.class);
@@ -172,6 +174,12 @@ public class GB {
context.getString(R.string.notification_channel_gps),
NotificationManager.IMPORTANCE_MIN);
notificationManager.createNotificationChannel(channelGps);
NotificationChannel channelPebbleJs = new NotificationChannel(
NOTIFICATION_CHANNEL_ID_PEBBLE_JS,
"Pebble JS runner",
NotificationManager.IMPORTANCE_MIN);
notificationManager.createNotificationChannel(channelPebbleJs);
}
notificationChannelsCreated = true;
@@ -17,7 +17,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.util.SparseArray;
import androidx.annotation.Nullable;
@@ -33,6 +36,9 @@ import java.io.IOException;
import java.util.Iterator;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.PebbleJsService;
public class PebbleUtils {
private static final Logger LOG = LoggerFactory.getLogger(PebbleUtils.class);
@@ -191,4 +197,32 @@ public class PebbleUtils {
}
return jsAppMessage.toString();
}
public static void startJsEngineForDevice(Context context, GBDevice device, UUID uuid) {
PebbleJsService service = PebbleJsService.Companion.getInstance();
if (service == null) {
LOG.warn("PebbleJsService not running yet, starting it");
PebbleJsService.Companion.startService(context);
// Delay start until service is ready
new Handler(Looper.getMainLooper()).postDelayed(() -> {
PebbleJsService s = PebbleJsService.Companion.getInstance();
if (s != null) {
s.startJsForDevice(device, uuid);
}
}, 600);
return;
}
service.startJsForDevice(device, uuid);
}
public static void stopJsEngineForDevice(GBDevice device) {
PebbleJsService service = PebbleJsService.Companion.getInstance();
if (service != null) {
service.stopJsForDevice(device);
}
}
}
@@ -1,169 +0,0 @@
/* 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.content.Context;
import android.content.MutableContextWrapper;
import android.os.Handler;
import android.os.Looper;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface;
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient;
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient;
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 WebViewSingleton() {
}
public static synchronized void ensureCreated(Context context, int requestType) {
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.resumeTimers();
instance.webView.setWebViewClient(new GBWebClient(requestType));
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);
//allow local js files access
webSettings.setAllowContentAccess(true);
webSettings.setAllowFileAccess(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
}
}
public static WebViewSingleton getInstance() {
return instance;
}
@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 Context context, @NonNull GBDevice device, @NonNull UUID uuid) {
ensureCreated(context, GBWebClient.REQUEST_TYPE_PEBBLE_BACKGROUND_JS);
InternetHelperSingleton.INSTANCE.ensureInternetHelperBound();
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(webView -> {
webView.onResume();
webView.removeJavascriptInterface("GBjs");
webView.addJavascriptInterface(jsInterface, "GBjs");
webView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
});
}
}
public void stopJavascriptInterface() {
invokeWebview(webView -> {
webView.removeJavascriptInterface("GBjs");
webView.loadUrl("about:blank");
});
}
public void disposeWebView() {
currentRunningUUID = null;
invokeWebview(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(() -> {
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);
}
}