diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java index f814ca7741..4ae4a62bf9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java @@ -79,9 +79,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtoco import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager; +import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton; import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; import nodomain.freeyourgadget.gadgetbridge.util.Version; -import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton; public abstract class AbstractAppManagerFragment extends Fragment { @@ -451,7 +451,7 @@ public abstract class AbstractAppManagerFragment extends Fragment { appListFabNew.show(); } if (appStoreActivity != null && mGBDevice.getDeviceCoordinator() instanceof PebbleCoordinator) { - if (WebViewSingleton.getInstance().ensureInternetHelperBound()) { + if (InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { appListFabStore.show(); } } @@ -482,7 +482,7 @@ public abstract class AbstractAppManagerFragment extends Fragment { } if (appStoreActivity != null && mGBDevice.getDeviceCoordinator() instanceof PebbleCoordinator) { - if (WebViewSingleton.getInstance().ensureInternetHelperBound()) { + if (InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { appListFabStore.setOnClickListener(v -> { Intent startIntent = new Intent(getContext(), appStoreActivity); startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); @@ -658,7 +658,7 @@ public abstract class AbstractAppManagerFragment extends Fragment { return true; } else if (itemId == R.id.appmanager_app_openinstore) { final String url = "https://apps.rebble.io/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1/?native=true&query=" + Uri.encode(selectedApp.getUUID().toString()); - if (WebViewSingleton.getInstance().ensureInternetHelperBound()) { + if (InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { final Intent startIntent = new Intent(getContext().getApplicationContext(), RebbleAppStoreActivity.class); startIntent.putExtra(DeviceService.EXTRA_URI, url); startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt index 3a9ee4e99c..f1a4c11f2c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt @@ -1,4 +1,4 @@ -/* Copyright (C) 2022-2024 Andreas Shimokawa, Daniel Dakhno, Gordon Williams, Arjan Schrijver +/* Copyright (C) 2025 Arjan Schrijver This file is part of Gadgetbridge. @@ -17,21 +17,15 @@ package nodomain.freeyourgadget.gadgetbridge.activities.appmanager import android.annotation.SuppressLint -import android.content.ComponentName import android.content.Intent import android.content.IntentFilter -import android.content.ServiceConnection import android.net.Uri import android.os.Bundle import android.os.Handler -import android.os.IBinder import android.os.Looper import android.os.ParcelFileDescriptor -import android.os.RemoteException import android.webkit.PermissionRequest -import android.webkit.WebChromeClient import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import android.widget.Toast @@ -41,25 +35,22 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice import nodomain.freeyourgadget.gadgetbridge.model.DeviceService -import nodomain.freeyourgadget.gadgetbridge.util.Capsule +import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBChromeClient +import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBWebClient import nodomain.freeyourgadget.gadgetbridge.util.GB +import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback -import nodomain.freeyourgadget.internethelper.aidl.http.IHttpService import org.json.JSONObject -import org.jsoup.Jsoup import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File import java.io.FileOutputStream import java.io.InputStreamReader import java.io.OutputStream -import java.nio.charset.Charset import java.nio.charset.StandardCharsets -import java.util.concurrent.CountDownLatch -import kotlin.concurrent.Volatile class RebbleAppStoreActivity : AbstractGBActivity() { val LOG: Logger = LoggerFactory.getLogger(RebbleAppStoreActivity::class.java) @@ -71,15 +62,6 @@ class RebbleAppStoreActivity : AbstractGBActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_banglejs_apps_management) - val intent1 = Intent("nodomain.freeyourgadget.internethelper.HttpService") - intent1.setPackage("nodomain.freeyourgadget.internethelper") - val res = applicationContext.bindService(intent1, mHttpConnection, BIND_AUTO_CREATE) - if (res) { - LOG.info("Bound to HttpService") - } else { - LOG.warn("Could not bind to HttpService") - } - val extras = intent.extras if (extras != null) { mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE) @@ -104,21 +86,6 @@ class RebbleAppStoreActivity : AbstractGBActivity() { finish() } - @Volatile - private var iHttpService: IHttpService? = null - - private val mHttpConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName?, service: IBinder?) { - LOG.info("onServiceConnected: {}", className) - iHttpService = IHttpService.Stub.asInterface(service) - } - - override fun onServiceDisconnected(className: ComponentName?) { - LOG.error("Service has unexpectedly disconnected: {}", className) - iHttpService = null - } - } - private fun isDownloadableWatchapp(url: String): Boolean { val downloadExtensions = listOf(".pbw", ".zip") return downloadExtensions.any { url.endsWith(it, ignoreCase = true) } @@ -127,7 +94,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() { private fun downloadInstallWatchapp(url: Uri) { val httpHeaders = HttpHeaders() val httpGetRequest = HttpGetRequest(url.toString(), httpHeaders) - iHttpService!!.get(httpGetRequest, object : IHttpCallback.Stub() { + InternetHelperSingleton.getHttpService()?.get(httpGetRequest, object : IHttpCallback.Stub() { override fun onResponse(response: HttpResponse) { val contentType = response.headers["content-type"]?.split(";")?.get(0) if (!contentType.equals("application/octet-stream") && !contentType.equals("application/zip")) { @@ -160,6 +127,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() { startIntent.setDataAndType(cacheFile.toUri(), null) startActivity(startIntent) } + override fun onException(message: String?) { GB.toast(getString(R.string.rebble_appstore_download_failed, message), Toast.LENGTH_LONG, GB.ERROR) } @@ -170,7 +138,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() { val appUrl = "https://appstore-api.rebble.io/api/v1/apps/id/$storeId" val httpHeaders = HttpHeaders() val httpGetRequest = HttpGetRequest(appUrl, httpHeaders) - iHttpService!!.get(httpGetRequest, object : IHttpCallback.Stub() { + InternetHelperSingleton.getHttpService()?.get(httpGetRequest, object : IHttpCallback.Stub() { override fun onResponse(response: HttpResponse) { val contentType = response.headers["content-type"]?.split(";")?.get(0) if (!contentType.equals("application/json")) { @@ -204,7 +172,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() { settings.loadWithOverviewMode = true settings.useWideViewPort = true - webView!!.webViewClient = object : WebViewClient() { + webView!!.webViewClient = object : GBWebClient() { override fun shouldOverrideUrlLoading( wv: WebView, request: WebResourceRequest @@ -229,85 +197,6 @@ class RebbleAppStoreActivity : AbstractGBActivity() { return true } - override fun shouldInterceptRequest( - view: WebView?, - request: WebResourceRequest - ): WebResourceResponse? { - LOG.info("shouldIntercept {} {} {}", request.method, request.url, iHttpService != null) - - if (!request.method.equals("get", ignoreCase = true)) { - return super.shouldInterceptRequest(view, request) - } - if (iHttpService == null) { - return super.shouldInterceptRequest(view, request) - } - - val httpHeaders = HttpHeaders() - for (header in request.requestHeaders.entries) { - httpHeaders.addHeader(header.key, header.value) - } - - val httpGetRequest = HttpGetRequest(request.url.toString(), httpHeaders) - val latch = CountDownLatch(1) - val internetResponseCapsule = Capsule() - - try { - iHttpService!!.get(httpGetRequest, object : IHttpCallback.Stub() { - @Throws(RemoteException::class) - override fun onResponse(response: HttpResponse) { - // Extract headers - val contentType = response.headers["content-type"]?.split(";")?.get(0) ?: "text/html" - val contentEncoding = response.headers["content-encoding"] ?: "UTF-8" - - // Retrieve original payload as InputStream - val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body) - - // Clean up malformed HTML from Rebble - if (contentType.equals("text/html", ignoreCase = true)) { - val rawHtml = inputStream.bufferedReader(Charset.forName(contentEncoding)).use { it.readText() } - val cleanedHtml = Jsoup.parse(rawHtml).html() - val internetResponse = WebResourceResponse( - contentType, - contentEncoding, - response.status, - "OK", - response.headers.toMap(), - cleanedHtml.byteInputStream() - ) - internetResponseCapsule.set(internetResponse) - } else { - // If not text/html, return original response - val internetResponse = WebResourceResponse( - contentType, - contentEncoding, - response.status, - "OK", - response.headers.toMap(), - inputStream - ) - internetResponseCapsule.set(internetResponse) - } - latch.countDown() - } - - @Throws(RemoteException::class) - override fun onException(message: String?) { - throw RuntimeException(message) - } - }) - } catch (e: RemoteException) { - throw RuntimeException(e) - } - - try { - latch.await() - } catch (e: InterruptedException) { - throw RuntimeException(e) - } - - return internetResponseCapsule.get() - } - override fun onReceivedError( view: WebView, errorCode: Int, @@ -328,7 +217,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() { webView!!.loadUrl(url) }, 100) - webView!!.webChromeClient = object : WebChromeClient() { + webView!!.webChromeClient = object : GBChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { request.grant(request.resources) } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java index b11c4ae16d..57a32fb03c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java @@ -50,7 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.URLFilterEntry; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather; import nodomain.freeyourgadget.gadgetbridge.model.weather.WeatherMapper; -import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton; +import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton; public class GBWebClient extends WebViewClient { @@ -63,7 +63,7 @@ public class GBWebClient extends WebViewClient { @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { Uri parsedUri = request.getUrl(); - LOG.debug("WEBVIEW shouldInterceptRequest URL: " + parsedUri.toString()); + LOG.debug("WEBVIEW shouldInterceptRequest URL: {}", parsedUri.toString()); WebResourceResponse mimickedReply = mimicReply(parsedUri); if (mimickedReply != null) return mimickedReply; @@ -72,7 +72,7 @@ public class GBWebClient extends WebViewClient { @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - LOG.debug("WEBVIEW shouldInterceptRequest URL (legacy): " + url); + LOG.debug("WEBVIEW shouldInterceptRequest URL (legacy): {}", url); Uri parsedUri = Uri.parse(url); WebResourceResponse mimickedReply = mimicReply(parsedUri); if (mimickedReply != null) @@ -85,6 +85,11 @@ public class GBWebClient extends WebViewClient { boolean urlIsAllowed = locallySupported; List urlFilterEntries = DBHelper.getURLFilterEntries(); + // Handle local schemes locally + if (requestedUri.toString().startsWith("file://") || requestedUri.toString().startsWith("gadgetbridge://")) { + return null; + } + // Search for matches boolean matchFound = false; for (URLFilterEntry entry : urlFilterEntries) { @@ -105,7 +110,7 @@ public class GBWebClient extends WebViewClient { } // Add to database if missing - if (requestedUri.getHost() != null && !requestedUri.toString().startsWith("file://") && !matchFound) { + if (requestedUri.getHost() != null && !matchFound) { LOG.info("URL not matched with URLFilterEntry, storing new entry"); String defaultAction = sharedPreferences.getString("pref_key_internethelper_new_url_action", "deny"); urlIsAllowed = defaultAction.equals("allow"); @@ -116,27 +121,27 @@ public class GBWebClient extends WebViewClient { } // Handle request - if (requestedUri.getHost() != null && !requestedUri.toString().startsWith("file://") && urlIsAllowed) { - if (!forceLocal && WebViewSingleton.getInstance().ensureInternetHelperBound()) { + if (requestedUri.getHost() != null && urlIsAllowed) { + if (!forceLocal && InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { LOG.debug("WEBVIEW forwarding request to the internet helper"); try { - return WebViewSingleton.getInstance().send(requestedUri); + return InternetHelperSingleton.INSTANCE.send(requestedUri); } catch (RemoteException | InterruptedException e) { - LOG.warn("Error downloading data from " + requestedUri, e); + LOG.warn("Error downloading data from {}", requestedUri, e); } } else { if (StringUtils.endsWith(requestedUri.getHost(), "openweathermap.org")){ - LOG.debug("WEBVIEW request to openweathermap.org detected of type: " + requestedUri.getPath() + " params: " + requestedUri.getQuery()); + LOG.debug("WEBVIEW request to openweathermap.org detected of type: {} params: {}", requestedUri.getPath(), requestedUri.getQuery()); return mimicOpenWeatherMapResponse(requestedUri.getPath(), requestedUri.getQueryParameter("units")); } else if (StringUtils.endsWith(requestedUri.getHost(), "rawgit.com")) { - LOG.debug("WEBVIEW request to rawgit.com detected of type: " + requestedUri.getPath() + " params: " + requestedUri.getQuery()); + LOG.debug("WEBVIEW request to rawgit.com detected of type: {} params: {}", requestedUri.getPath(), requestedUri.getQuery()); return mimicRawGitResponse(requestedUri.getPath()); } else { - LOG.debug("WEBVIEW request to allowed domain detected but not intercepted: " + requestedUri); + LOG.debug("WEBVIEW request to allowed domain detected but not intercepted: {}", requestedUri); } } } else { - LOG.debug("WEBVIEW request not intercepted:" + requestedUri); + LOG.debug("WEBVIEW request not intercepted:{}", requestedUri); } return null; } @@ -146,21 +151,33 @@ public class GBWebClient extends WebViewClient { Uri parsedUri = Uri.parse(url); if (parsedUri.getScheme().startsWith("http")) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - GBApplication.getContext().startActivity(i); + if (InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { + view.loadUrl(url); + } else { + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + GBApplication.getContext().startActivity(i); + } + } else if (parsedUri.getScheme().startsWith("gadgetbridge")) { + url = url.replaceFirst("^gadgetbridge://.*json=", "file:///android_asset/app_config/configure.html?config=true&json="); + view.loadUrl(url); } else if (parsedUri.getScheme().startsWith("pebblejs")) { url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json="); view.loadUrl(url); } else if (parsedUri.getScheme().equals("data")) { //clay view.loadUrl(url); } else { - LOG.debug("WEBVIEW Ignoring unhandled scheme: " + parsedUri.getScheme()); + LOG.debug("WEBVIEW Ignoring unhandled scheme: {}", parsedUri.getScheme()); } return true; } + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return shouldOverrideUrlLoading(view, request.getUrl().toString()); + } + private WebResourceResponse mimicRawGitResponse(String path) { if("/aHcVolle/TrekVolle/master/online.html".equals(path)) { //TrekVolle online check Map headers = new HashMap<>(); @@ -211,11 +228,11 @@ public class GBWebClient extends WebViewClient { // // resp.put("cod", 200); } else { - LOG.warn("WEBVIEW - cannot mimick request of type " + type + " (unsupported or lack of data)"); + LOG.warn("WEBVIEW - cannot mimick request of type {} (unsupported or lack of data)", type); return null; } - LOG.info("WEBVIEW - mimic openweather response" + resp.toString()); + LOG.info("WEBVIEW - mimic openweather response {}", resp.toString()); Map headers = new HashMap<>(); headers.put("Access-Control-Allow-Origin", "*"); @@ -266,7 +283,7 @@ public class GBWebClient extends WebViewClient { LOG.error("Error while reconstructing OWM weather reply"); return null; } - LOG.debug("Weather JSON for WEBVIEW: " + reconstructedOWMWeather); + LOG.debug("Weather JSON for WEBVIEW: {}", reconstructedOWMWeather); return reconstructedOWMWeather; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetHelperSingleton.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetHelperSingleton.kt new file mode 100644 index 0000000000..252e1b7ee9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetHelperSingleton.kt @@ -0,0 +1,150 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.util + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.net.Uri +import android.os.IBinder +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import android.webkit.WebResourceResponse +import nodomain.freeyourgadget.gadgetbridge.GBApplication +import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest +import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders +import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse +import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback +import nodomain.freeyourgadget.internethelper.aidl.http.IHttpService +import org.jsoup.Jsoup +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.nio.charset.Charset +import java.util.concurrent.CountDownLatch + +object InternetHelperSingleton { + private val LOG: Logger = LoggerFactory.getLogger(InternetHelperSingleton::class.java) + private var internetHelperBound = false + private var internetHelper: IHttpService? = null + + fun getHttpService(): IHttpService? { + ensureInternetHelperBound() + return internetHelper + } + + //Internet helper outgoing connection + private val internetHelperConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName?, service: IBinder?) { + LOG.info("internet helper service bound") + internetHelperBound = true + internetHelper = IHttpService.Stub.asInterface(service) + } + + override fun onServiceDisconnected(className: ComponentName?) { + LOG.info("internet helper service unbound") + internetHelper = null + internetHelperBound = false + } + } + + fun ensureInternetHelperBound(): Boolean { + val context = GBApplication.getContext() + if (!internetHelperBound) { + val internetHelperPkg = "nodomain.freeyourgadget.internethelper" + val internetHelperCls = "$internetHelperPkg.HttpService" + try { + context.packageManager.getApplicationInfo(internetHelperPkg, 0) + val intent = Intent() + intent.component = ComponentName(internetHelperPkg, internetHelperCls) + + val intent1 = Intent("nodomain.freeyourgadget.internethelper.HttpService") + intent1.setPackage("nodomain.freeyourgadget.internethelper") + context.bindService(intent1, internetHelperConnection, Context.BIND_AUTO_CREATE) + LOG.info("WEBVIEW: Internet helper bound successfully.") + } catch (_: PackageManager.NameNotFoundException) { + LOG.info("WEBVIEW: Internet helper not installed, only mimicked HTTP requests will work.") + } catch (_: SecurityException) { + LOG.info("WEBVIEW: Permission for internet helper not granted, only mimicked HTTP requests will work.") + } + } + return internetHelperBound + } + + @Throws(RemoteException::class, InterruptedException::class) + fun send(webRequest: Uri): WebResourceResponse? { + val httpHeaders = HttpHeaders() + val httpGetRequest = HttpGetRequest(webRequest.toString(), httpHeaders) + val latch = CountDownLatch(1) + val internetResponseCapsule = Capsule() + try { + internetHelper?.get(httpGetRequest, object : IHttpCallback.Stub() { + @Throws(RemoteException::class) + override fun onResponse(response: HttpResponse) { + // Extract headers + val contentType = response.headers["content-type"]?.split(";")?.get(0) ?: "text/html" + val contentEncoding = response.headers["content-encoding"] ?: "UTF-8" + + // Retrieve original payload as InputStream + val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body) + + // Clean up and fix received malformed HTML + if (contentType.equals("text/html", ignoreCase = true)) { + val rawHtml = inputStream.bufferedReader(Charset.forName(contentEncoding)).use { it.readText() } + val cleanedHtml = Jsoup.parse(rawHtml).html() + val internetResponse = WebResourceResponse( + contentType, + contentEncoding, + response.status, + "OK", + response.headers.toMap(), + cleanedHtml.byteInputStream() + ) + internetResponseCapsule.set(internetResponse) + } else { + // If not text/html, return original response + val internetResponse = WebResourceResponse( + contentType, + contentEncoding, + response.status, + "OK", + response.headers.toMap(), + inputStream + ) + internetResponseCapsule.set(internetResponse) + } + latch.countDown() + } + + @Throws(RemoteException::class) + override fun onException(message: String?) { + throw RuntimeException(message) + } + }) + } catch (e: RemoteException) { + throw RuntimeException(e) + } + try { + latch.await() + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + + return internetResponseCapsule.get() + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java index f090a52f1b..0831b5079c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java @@ -18,19 +18,10 @@ 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.net.Uri; import android.os.Handler; -import android.os.IBinder; import android.os.Looper; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; @@ -40,17 +31,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; -import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest; -import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders; -import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse; -import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback; -import nodomain.freeyourgadget.internethelper.aidl.http.IHttpService; public class WebViewSingleton { @@ -61,27 +46,10 @@ public class WebViewSingleton { private MutableContextWrapper contextWrapper; private Looper mainLooper; private UUID currentRunningUUID; - private IHttpService internetHelper = null; - public boolean internetHelperBound; 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 = IHttpService.Stub.asInterface(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); @@ -107,69 +75,10 @@ public class WebViewSingleton { } } - public boolean ensureInternetHelperBound() { - if (contextWrapper != null && !internetHelperBound) { - String internetHelperPkg = "nodomain.freeyourgadget.internethelper"; - String internetHelperCls = internetHelperPkg + ".HttpService"; - try { - contextWrapper.getPackageManager().getApplicationInfo(internetHelperPkg, 0); - Intent intent = new Intent(); - intent.setComponent(new ComponentName(internetHelperPkg, internetHelperCls)); - - final Intent intent1 = new Intent("nodomain.freeyourgadget.internethelper.HttpService"); - intent1.setPackage("nodomain.freeyourgadget.internethelper"); - contextWrapper.getApplicationContext().bindService(intent1, internetHelperConnection, Context.BIND_AUTO_CREATE); - LOG.info("WEBVIEW: Internet helper bound successfully."); - } catch (PackageManager.NameNotFoundException e) { - LOG.info("WEBVIEW: Internet helper not installed, only mimicked HTTP requests will work."); - } catch (SecurityException e) { - LOG.info("WEBVIEW: Permission for internet helper not granted, only mimicked HTTP requests will work."); - } - } - return internetHelperBound; - } - public static WebViewSingleton getInstance() { return instance; } - public WebResourceResponse send(Uri webRequest) throws RemoteException, InterruptedException { - final HttpHeaders httpHeaders = new HttpHeaders(); - final HttpGetRequest httpGetRequest = new HttpGetRequest(webRequest.toString(), httpHeaders); - final CountDownLatch latch = new CountDownLatch(1); - final Capsule internetResponseCapsule = new Capsule<>(); - try { - internetHelper.get(httpGetRequest, new IHttpCallback.Stub() { - @Override - public void onResponse(HttpResponse response) throws RemoteException { - response.getHeaders().addHeader("Access-Control-Allow-Origin", "*"); - WebResourceResponse internetResponse = new WebResourceResponse( - response.getHeaders().get("content-type"), - response.getHeaders().get("content-encoding"), - response.getStatus(), "OK", - response.getHeaders().toMap(), - new ParcelFileDescriptor.AutoCloseInputStream(response.getBody()) - ); - internetResponseCapsule.set(internetResponse); - latch.countDown(); - } - - @Override - public void onException(String message) throws RemoteException { - throw new RuntimeException(message); - } - }); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - return internetResponseCapsule.get(); - } @NonNull public WebView getWebView(Context context) { @@ -210,7 +119,7 @@ public class WebViewSingleton { webView.addJavascriptInterface(jsInterface, "GBjs"); webView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500); }); - ensureInternetHelperBound(); + InternetHelperSingleton.INSTANCE.ensureInternetHelperBound(); } } @@ -222,11 +131,6 @@ public class WebViewSingleton { } public void disposeWebView() { - if (internetHelperBound) { - LOG.debug("WEBVIEW: will unbind the internet helper"); - contextWrapper.getApplicationContext().unbindService(internetHelperConnection); - internetHelperBound = false; - } currentRunningUUID = null; invokeWebview(webView -> { webView.removeJavascriptInterface("GBjs");