mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Centalize internet helper code
This commit is contained in:
committed by
Arjan Schrijver
parent
41ab2ae09e
commit
c2c67a3d78
+4
-4
@@ -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);
|
||||
|
||||
+9
-120
@@ -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)
|
||||
}
|
||||
|
||||
+33
-16
@@ -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")) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+150
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user