Centalize internet helper code

This commit is contained in:
Arjan Schrijver
2025-12-25 14:45:08 +01:00
committed by Arjan Schrijver
parent 41ab2ae09e
commit c2c67a3d78
5 changed files with 200 additions and 240 deletions
@@ -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);
@@ -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<WebResourceResponse?>()
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)
}
@@ -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<URLFilterEntry> 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<String, String> 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<String, String> 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;
}
@@ -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 <https://www.gnu.org/licenses/>. */
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<WebResourceResponse?>()
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()
}
}
@@ -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<WebResourceResponse> 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");