mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Replace background WebViewSingleton with proper foreground service
This commit is contained in:
committed by
Arjan Schrijver
parent
2259e6ebb5
commit
775d064262
@@ -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
|
||||
|
||||
+8
-68
@@ -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) {
|
||||
|
||||
-9
@@ -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) {
|
||||
|
||||
+2
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
+18
-28
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
+170
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user