diff --git a/app/build.gradle b/app/build.gradle
index 91290837ca..58b43ac8dd 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -268,7 +268,6 @@ dependencies {
implementation 'com.github.wax911.android-emojify:emojify:1.9.7'
implementation 'com.github.wax911.android-emojify:gson:1.9.7'
implementation 'com.google.protobuf:protobuf-javalite:4.33.2'
- implementation 'com.android.volley:volley:1.2.1'
implementation 'com.squareup.okhttp3:okhttp:5.2.1'
implementation 'org.msgpack:msgpack-core:0.9.10'
implementation 'com.github.ByteHamster:SearchPreference:2.7.3'
diff --git a/app/src/main/assets/licenses/licenses.json b/app/src/main/assets/licenses/licenses.json
index ac55db0261..2c7c728cec 100644
--- a/app/src/main/assets/licenses/licenses.json
+++ b/app/src/main/assets/licenses/licenses.json
@@ -63,10 +63,10 @@
"path": "licenses/desugar_jdk_libs.txt"
},
{
- "name": "volley",
- "owner": "Google",
+ "name": "okhttp",
+ "owner": "Square, Inc.",
"type": "Apache 2.0",
- "url": "https://github.com/google/volley",
+ "url": "https://github.com/square/okhttp",
"path": "licenses/apache-2.0.txt"
},
{
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
index d7246bc8ae..93892d2933 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java
@@ -58,10 +58,6 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
private Uri confUri;
- /**
- * When bgjs is enabled, this field refers to the WebViewSingleton,
- * otherwise it refers to the legacy webview from the activity_legacy_external_pebble_js layout
- */
private WebView myWebView;
@Override
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 c352ffc267..9ce41c4354 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
@@ -23,41 +23,24 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
-import android.os.ParcelFileDescriptor
import android.webkit.PermissionRequest
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.widget.Toast
import androidx.core.net.toUri
-import com.android.volley.Request
-import com.android.volley.toolbox.JsonObjectRequest
-import com.android.volley.toolbox.Volley
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
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.GB
-import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton
+import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient
-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 okhttp3.OkHttpClient
import org.json.JSONObject
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.StandardCharsets
class RebbleAppStoreActivity : AbstractGBActivity() {
val LOG: Logger = LoggerFactory.getLogger(RebbleAppStoreActivity::class.java)
@@ -99,148 +82,24 @@ class RebbleAppStoreActivity : AbstractGBActivity() {
}
private fun downloadInstallWatchapp(url: Uri) {
- if (GBApplication.hasDirectInternetAccess()) {
- downloadBinaryFile(url) { file ->
- installFile(file)
- }
- } else {
- val httpHeaders = HttpHeaders()
- val httpGetRequest = HttpGetRequest(url.toString(), httpHeaders)
- 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")) {
- GB.toast(
- getString(
- R.string.rebble_appstore_download_failed_wrong_content_type,
- contentType
- ),
- Toast.LENGTH_LONG, GB.ERROR
- )
- LOG.error("Received content-type $contentType but expected application/octet-stream or application/zip")
- return
- }
- val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
- val filename = url.lastPathSegment ?: "downloaded_file.pbw"
- val cacheDir = applicationContext.externalCacheDir ?: return
- val cacheFile = File(cacheDir, filename)
- val outputStream: OutputStream = FileOutputStream(cacheFile)
- val buffer = ByteArray(1024)
- var len: Int
- while (inputStream.read(buffer).also { len = it } > 0) {
- outputStream.write(buffer, 0, len)
- }
- outputStream.flush()
- outputStream.close()
- installFile(cacheFile)
- }
+ val filename = url.lastPathSegment ?: "downloaded_file.bin"
+ val cacheDir = applicationContext.externalCacheDir
+ val targetFile = File(cacheDir, filename)
- override fun onException(message: String?) {
- GB.toast(
- getString(R.string.rebble_appstore_download_failed, message),
- Toast.LENGTH_LONG,
- GB.ERROR
- )
- LOG.error("Failed downloading file: $message")
- }
- })
+ InternetUtils.downloadBinaryFile(url, targetFile) { file ->
+ installFile(file)
}
}
private fun downloadInstallWatchappById(storeId: String) {
val appUrl = "https://appstore-api.rebble.io/api/v1/apps/id/$storeId"
- if (GBApplication.hasDirectInternetAccess()) {
- val requestQueue = Volley.newRequestQueue(this)
- val jsonObjectRequest = JsonObjectRequest(
- Request.Method.GET, appUrl, null,
- { response ->
- val dataArray = response.getJSONArray("data")
- val firstAppObject = dataArray.getJSONObject(0)
- val latestRelease = firstAppObject.getJSONObject("latest_release")
- val pbwFile = latestRelease.getString("pbw_file")
- downloadInstallWatchapp(pbwFile.toUri())
- },
- { error ->
- GB.toast(
- getString(
- R.string.rebble_appstore_fetching_download_file_failed,
- error
- ), Toast.LENGTH_LONG, GB.ERROR
- )
- LOG.error("Failed fetching download file URL", error)
- }
- )
- requestQueue.add(jsonObjectRequest)
- } else {
- val httpHeaders = HttpHeaders()
- val httpGetRequest = HttpGetRequest(appUrl, httpHeaders)
- 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")) {
- GB.toast(
- getString(
- R.string.rebble_appstore_fetch_app_info_failed_content_type,
- contentType
- ),
- Toast.LENGTH_LONG, GB.ERROR
- )
- LOG.error("Received content-type $contentType but expected application/json")
- return
- }
- val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
- val responseBody =
- InputStreamReader(inputStream, StandardCharsets.UTF_8).readText()
- val jsonObject = JSONObject(responseBody)
- val dataArray = jsonObject.getJSONArray("data")
- val firstAppObject = dataArray.getJSONObject(0)
- val latestRelease = firstAppObject.getJSONObject("latest_release")
- val pbwFile = latestRelease.getString("pbw_file")
- downloadInstallWatchapp(pbwFile.toUri())
- }
-
- override fun onException(message: String?) {
- GB.toast(
- getString(
- R.string.rebble_appstore_fetching_download_file_failed,
- message
- ), Toast.LENGTH_LONG, GB.ERROR
- )
- LOG.error("Error downloading file: $message")
- }
- })
- }
- }
-
- fun downloadBinaryFile(url: Uri, onComplete: (File) -> Unit) {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- val client = OkHttpClient()
- val request = okhttp3.Request.Builder().url(url.toString()).build()
- val response = client.newCall(request).execute()
-
- if (!response.isSuccessful) {
- LOG.error("Downloading $url failed: ${response.code}")
- return@launch
- }
-
- val filename = url.lastPathSegment ?: "downloaded_file.pbw"
- val cacheDir = applicationContext.externalCacheDir
- val cacheFile = File(cacheDir, filename)
- val outputStream = FileOutputStream(cacheFile)
-
- val inputStream = response.body.byteStream()
- inputStream.copyTo(outputStream)
- outputStream.flush()
- outputStream.close()
- inputStream.close()
-
- onComplete(cacheFile)
- } catch (e: Exception) {
- LOG.error("Downloading $url failed: ", e)
- }
+ val response: JSONObject? = InternetUtils.doRequest(appUrl.toUri())
+ if (response != null) {
+ val dataArray = response.getJSONArray("data")
+ val firstAppObject = dataArray.getJSONObject(0)
+ val latestRelease = firstAppObject.getJSONObject("latest_release")
+ val pbwFile = latestRelease.getString("pbw_file")
+ downloadInstallWatchapp(pbwFile.toUri())
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java
index f638ceafea..d15cc2912f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java
@@ -32,11 +32,8 @@ import android.webkit.DownloadListener;
import android.webkit.JavascriptInterface;
import android.webkit.PermissionRequest;
import android.webkit.ValueCallback;
-import android.webkit.WebChromeClient;
-import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -55,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient;
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient;
public class AppsManagementActivity extends AbstractGBActivity {
@@ -195,7 +193,6 @@ public class AppsManagementActivity extends AbstractGBActivity {
private void initViews() {
//https://stackoverflow.com/questions/4325639/android-calling-javascript-functions-in-webview
webView = findViewById(R.id.webview);
- webView.setWebViewClient(new WebViewClient());
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDatabaseEnabled(true);
@@ -212,12 +209,6 @@ public class AppsManagementActivity extends AbstractGBActivity {
public void onPageFinished(WebView view, String weburl){
//webView.loadUrl("javascript:showToast('WebView in Espruino')");
}
- @Override
- public boolean shouldOverrideUrlLoading(WebView vw, WebResourceRequest request) {
- Intent intent = new Intent(Intent.ACTION_VIEW, request.getUrl());
- vw.getContext().startActivity(intent);
- return true;
- }
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
@@ -237,7 +228,7 @@ public class AppsManagementActivity extends AbstractGBActivity {
}, 1000);
- webView.setWebChromeClient(new WebChromeClient() {
+ webView.setWebChromeClient(new GBChromeClient() {
@Override
public void onPermissionRequest(final PermissionRequest request) {
request.grant(request.getResources());
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java
index eda2f1000d..791e35fb30 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java
@@ -55,7 +55,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
-import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton;
public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java
index 69e5e1af23..5c7590a9ea 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java
@@ -19,6 +19,7 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs;
+import static java.util.Collections.emptyMap;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP_SIZE;
@@ -58,21 +59,9 @@ import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import com.android.volley.AuthFailureError;
-import com.android.volley.DefaultRetryPolicy;
-import com.android.volley.Request;
-import com.android.volley.RequestQueue;
-import com.android.volley.Response;
-import com.android.volley.VolleyError;
-import com.android.volley.toolbox.StringRequest;
-import com.android.volley.toolbox.Volley;
-
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -151,11 +140,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
-import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton;
+import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
-import nodomain.freeyourgadget.gadgetbridge.util.VolleyUtils;
public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(BangleJSDeviceSupport.class);
@@ -189,10 +177,6 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
// this stores the globalUartReceiver (for uart.tx intents)
private BroadcastReceiver globalUartReceiver = null;
- // used to make HTTP requests and handle responses
- private RequestQueue requestQueue = null;
- private RequestQueue insecureRequestQueue = null;
-
/// Maximum amount of characters to store in receiveHistory
public static final int MAX_RECEIVE_HISTORY_CHARS = 100000;
/// Used to avoid spamming logs with ACTION_DEVICE_CHANGED messages
@@ -226,7 +210,6 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
super.dispose();
stopGlobalUartReceiver();
stopLocationUpdate();
- stopRequestQueue();
handler.removeCallbacksAndMessages(null);
}
}
@@ -246,38 +229,6 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
gpsUpdateSetup = false;
}
- private void stopRequestQueue() {
- if (requestQueue != null) {
- requestQueue.stop();
- }
- if (insecureRequestQueue != null) {
- insecureRequestQueue.stop();
- }
- }
-
- private RequestQueue getRequestQueue(final boolean insecure) {
- if (insecure) {
- if (insecureRequestQueue == null) {
- try {
- insecureRequestQueue = Volley.newRequestQueue(
- getContext(),
- VolleyUtils.createInsecureHurlStack()
- );
- } catch (final Exception e) {
- LOG.error("Failed to initialized insecure request queue", e);
- // fallback to secure one
- return getRequestQueue(false);
- }
- }
- return insecureRequestQueue;
- } else {
- if (requestQueue == null) {
- requestQueue = Volley.newRequestQueue(getContext());
- }
- return requestQueue;
- }
- }
-
private void addReceiveHistory(String s) {
receiveHistory += s;
if (receiveHistory.length() > MAX_RECEIVE_HISTORY_CHARS)
@@ -924,123 +875,37 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport {
}
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
- if (! devicePrefs.getBoolean(PREF_DEVICE_INTERNET_ACCESS, false)) {
+ if (!devicePrefs.getBoolean(PREF_DEVICE_INTERNET_ACCESS, false)) {
uartTxJSONError("http", "Internet access not enabled for this watch", id);
return;
}
String url = json.getString("url");
final boolean insecure = json.optBoolean("insecure", false);
- int method = Request.Method.GET;
- if (json.has("method")) {
- String m = json.getString("method").toLowerCase(Locale.US);
- if (m.equals("get")) method = Request.Method.GET;
- else if (m.equals("post")) method = Request.Method.POST;
- else if (m.equals("head")) method = Request.Method.HEAD;
- else if (m.equals("put")) method = Request.Method.PUT;
- else if (m.equals("patch")) method = Request.Method.PATCH;
- else if (m.equals("delete")) method = Request.Method.DELETE;
- else uartTxJSONError("http", "Unknown HTTP method "+m,id);
- }
+ String method = json.getString("method").toUpperCase(Locale.US);
- byte[] _body = null;
+ String body = null;
if (json.has("body"))
- _body = json.getString("body").getBytes();
- final byte[] body = _body;
+ body = json.getString("body");
- Map _headers = null;
+ Map headers = null;
if (json.has("headers")) {
JSONObject h = json.getJSONObject("headers");
- _headers = new HashMap();
+ headers = new HashMap();
Iterator iter = h.keys();
while (iter.hasNext()) {
String key = iter.next();
try {
String value = h.getString(key);
- _headers.put(key, value);
+ headers.put(key, value);
} catch (JSONException e) {
}
}
}
- final Map headers = _headers;
+ if (headers == null) headers = emptyMap();
- String _xmlPath = "";
- String _xmlReturn = "";
- try {
- _xmlPath = json.getString("xpath");
- _xmlReturn = json.getString("return");
- } catch (JSONException e) {
- }
- final String xmlPath = _xmlPath;
- final String xmlReturn = _xmlReturn;
- // Request a string response from the provided URL.
- StringRequest stringRequest = new StringRequest(method, url,
- new Response.Listener() {
- @Override
- public void onResponse(String response) {
- JSONObject o = new JSONObject();
- if (xmlPath.length() != 0) {
- try {
- Document doc = Jsoup.parse(response);
- Elements result = doc.selectXpath(xmlPath);
- if (xmlReturn.equals("array")) {
- response = null; // don't add it below
- JSONArray arr = new JSONArray();
- for (int i = 0; i < result.size(); i++)
- arr.put(result.get(i).text());
- o.put("resp", arr);
- } else { // else return only first!
- response = "";
- if (!result.isEmpty())
- response = result.get(0).text();
- }
- } catch (Exception error) {
- uartTxJSONError("http", error.toString(), id);
- return;
- }
- }
- try {
- o.put("t", "http");
- if( id!=null)
- o.put("id", id);
- if (response!=null)
- o.put("resp", response);
- } catch (JSONException e) {
- GB.toast(getContext(), "HTTP: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
- }
- uartTxJSON("http", o);
- }
- }, new Response.ErrorListener() {
- @Override
- public void onErrorResponse(VolleyError error) {
- uartTxJSONError("http", error.toString(), id);
- }
- }) {
- @Override
- public byte[] getBody() throws AuthFailureError {
- if (body == null) return super.getBody();
- return body;
- }
-
- @Override
- public Map getHeaders() throws AuthFailureError {
- // clone the data from super.getHeaders() so we can write to it
- Map h = new HashMap<>(super.getHeaders());
- if (headers != null) {
- for (String key : headers.keySet()) {
- String value = headers.get(key);
- h.put(key, value);
- }
- }
- return h;
- }
- };
- if (json.has("timeout")) {
- int timeout = json.getInt("timeout");
- stringRequest.setRetryPolicy(new DefaultRetryPolicy(timeout, 0, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
- }
- RequestQueue queue = getRequestQueue(insecure);
- queue.add(stringRequest);
+ JSONObject o = InternetUtils.Companion.doRequest(Uri.parse(url), method, headers, body, "application/json", insecure);
+ uartTxJSON("http", o);
}
/**
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetHelperSingleton.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetHelperSingleton.kt
index d18bdfca34..9e08a7b08a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetHelperSingleton.kt
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetHelperSingleton.kt
@@ -43,12 +43,6 @@ object InternetHelperSingleton {
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")
@@ -63,8 +57,9 @@ object InternetHelperSingleton {
}
}
- fun isInternetHelperBound(): Boolean {
- return internetHelperBound
+ fun getHttpService(): IHttpService? {
+ ensureInternetHelperBound()
+ return internetHelper
}
fun ensureInternetHelperBound(): Boolean {
@@ -92,64 +87,66 @@ object InternetHelperSingleton {
@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()
+ var result: WebResourceResponse? = null
+ val request = HttpGetRequest(webRequest.toString(), HttpHeaders())
+
LOG.debug("Forwarding GET request to {} to internet helper app", webRequest)
try {
- internetHelper?.get(httpGetRequest, object : IHttpCallback.Stub() {
- @Throws(RemoteException::class)
+ internetHelper?.get(request, object : IHttpCallback.Stub() {
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"
+ try {
+ val headers = response.headers.toMap()
+ val contentType = response.headers["content-type"]
+ ?.substringBefore(";")
+ ?.trim()
+ ?: "text/html"
+ val encoding = response.headers["content-encoding"] ?: "UTF-8"
+ val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
- // Retrieve original payload as InputStream
- val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
+ result = if (contentType.equals("text/html", ignoreCase = true)) {
+ val rawHtml = inputStream.bufferedReader(Charset.forName(encoding)).use { it.readText() }
+ val cleanedHtml = Jsoup.parse(rawHtml).html()
+ WebResourceResponse(
+ contentType,
+ encoding,
+ response.status,
+ "OK",
+ headers,
+ cleanedHtml.byteInputStream()
+ )
+ } else {
+ WebResourceResponse(
+ contentType,
+ encoding,
+ response.status,
+ "OK",
+ headers,
+ inputStream
+ )
+ }
- // 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)
+ } catch (e: Exception) {
+ LOG.error("Error processing HTTP response", e)
+ } finally {
+ latch.countDown()
}
- latch.countDown()
}
- @Throws(RemoteException::class)
override fun onException(message: String?) {
LOG.error("Error during GET request: $message")
+ result = null
+ latch.countDown()
}
})
+
} catch (e: RemoteException) {
- LOG.error("Error during GET request", e)
- }
- try {
- latch.await()
- } catch (e: InterruptedException) {
- LOG.error("Error during GET request", e)
+ LOG.error("Error initiating GET request", e)
+ latch.countDown()
+ return null
}
- return internetResponseCapsule.get()
+ latch.await()
+ return result
}
}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetUtils.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetUtils.kt
new file mode 100644
index 0000000000..132d634217
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/InternetUtils.kt
@@ -0,0 +1,206 @@
+/* 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.net.Uri
+import android.webkit.WebResourceResponse
+import nodomain.freeyourgadget.gadgetbridge.GBApplication
+import okhttp3.Headers
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.json.JSONObject
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+
+class InternetUtils {
+
+ companion object {
+ private val LOG: Logger = LoggerFactory.getLogger(InternetUtils::class.java)
+ private val defaultClient = OkHttpClient()
+
+ /**
+ * Performs an HTTP request to the given URI, optionally allowing insecure connections.
+ */
+ fun doRequest(
+ uri: Uri,
+ method: String = "GET",
+ requestHeaders: Map = emptyMap(),
+ body: String? = null,
+ bodyContentType: String = "text/plain",
+ insecure: Boolean = false
+ ): JSONObject? {
+ val response: WebResourceResponse? = if (GBApplication.hasDirectInternetAccess()) {
+ directRequest(uri, method, requestHeaders, body, bodyContentType, insecure)
+ } else {
+ InternetHelperSingleton.send(uri)
+ }
+ if (response == null) return null
+
+ // Convert response InputStream to String
+ val text = response.data.bufferedReader().use { it.readText() }
+ return JSONObject(text)
+ }
+
+ fun downloadBinaryFile(
+ uri: Uri,
+ targetFile: File,
+ onComplete: (File) -> Unit
+ ) {
+ try {
+ val response: WebResourceResponse? = if (GBApplication.hasDirectInternetAccess()) {
+ directRequest(
+ uri = uri,
+ method = "GET",
+ requestHeaders = emptyMap(),
+ body = null,
+ bodyContentType = "application/octet-stream",
+ insecure = false
+ )
+ } else {
+ InternetHelperSingleton.send(uri)
+ }
+
+ response?.data?.use { input ->
+ FileOutputStream(targetFile).use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ if (response != null)
+ onComplete(targetFile)
+ } catch (e: Exception) {
+ LOG.error("Downloading $uri failed: ", e)
+ }
+ }
+
+ /**
+ * Direct HTTP request using OkHttp.
+ */
+ private fun directRequest(
+ uri: Uri,
+ method: String,
+ requestHeaders: Map,
+ body: String?,
+ bodyContentType: String,
+ insecure: Boolean
+ ): WebResourceResponse {
+
+ val client = if (insecure) createInsecureClient() else defaultClient
+
+ val builder = Request.Builder().url(uri.toString())
+
+ // Apply request headers
+ for ((key, value) in requestHeaders) {
+ builder.addHeader(key, value)
+ }
+
+ // Convert body string to RequestBody if allowed
+ val requestBody: RequestBody? =
+ if (body != null && method.uppercase() !in listOf("GET", "HEAD")) {
+ body.toRequestBody(bodyContentType.toMediaType())
+ } else null
+
+ // Configure HTTP method
+ when (method.uppercase()) {
+ "GET" -> builder.get()
+ "HEAD" -> builder.head()
+ "DELETE" -> if (requestBody != null) builder.delete(requestBody) else builder.delete()
+ else -> builder.method(method.uppercase(), requestBody)
+ }
+
+ // Execute request
+ client.newCall(builder.build()).execute().use { response ->
+ val statusCode = response.code
+ val message = if (!response.message.isEmpty()) response.message else "OK"
+ val headers = response.headers.toMap()
+
+ val contentType = response.header("content-type") ?: "application/octet-stream"
+ val encoding = response.header("content-encoding") ?: "UTF-8"
+
+ // HEAD: empty body
+ if (method.equals("HEAD", true)) {
+ return WebResourceResponse(
+ contentType,
+ encoding,
+ statusCode,
+ message,
+ headers,
+ ByteArrayInputStream(ByteArray(0))
+ )
+ }
+
+ return WebResourceResponse(
+ contentType,
+ encoding,
+ statusCode,
+ message,
+ headers,
+ ByteArrayInputStream(response.body.bytes())
+ )
+ }
+ }
+
+ /**
+ * Converts OkHttp Headers to a Map suitable for WebResourceResponse.
+ */
+ private fun Headers.toMap(): Map {
+ val result = LinkedHashMap()
+ for (name in this.names()) {
+ result[name] = this.values(name).joinToString(",")
+ }
+ return result
+ }
+
+ /**
+ * Creates an OkHttpClient that accepts all SSL certificates (insecure).
+ */
+ private fun createInsecureClient(): OkHttpClient {
+ return try {
+ val trustAllCerts = arrayOf(
+ object : X509TrustManager {
+ override fun checkClientTrusted(chain: Array?, authType: String?) {}
+ override fun checkServerTrusted(chain: Array?, authType: String?) {}
+ override fun getAcceptedIssuers(): Array = arrayOf()
+ }
+ )
+
+ val sslContext = SSLContext.getInstance("TLS")
+ sslContext.init(null, trustAllCerts, SecureRandom())
+ val sslSocketFactory = sslContext.socketFactory
+
+ OkHttpClient.Builder()
+ .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
+ .hostnameVerifier { _, _ -> true }
+ .build()
+ } catch (e: Exception) {
+ LOG.error("Failed to create insecure OkHttp client", e)
+ defaultClient
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/VolleyUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/VolleyUtils.java
deleted file mode 100644
index c7a3c88c18..0000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/VolleyUtils.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/* Copyright (C) 2025 José Rebelo
-
- 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.annotation.SuppressLint;
-
-import com.android.volley.toolbox.HurlStack;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.security.SecureRandom;
-import java.security.cert.X509Certificate;
-
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
-public class VolleyUtils {
- public static HurlStack createInsecureHurlStack() throws Exception {
- // Trust all certificates
- final TrustManager[] trustAllCerts = new TrustManager[]{new InsecureTrustManager()};
- final SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(null, trustAllCerts, new SecureRandom());
-
- return new HurlStack(null, sslContext.getSocketFactory()) {
- @Override
- protected HttpURLConnection createConnection(final URL url) throws IOException {
- final HttpURLConnection httpURLConnection = super.createConnection(url);
- if (httpURLConnection instanceof HttpsURLConnection httpsURLConnection) {
- // Trust all hostnames
- httpsURLConnection.setHostnameVerifier((hostname, session) -> true);
- }
- return httpURLConnection;
- }
- };
- }
-
- @SuppressLint("CustomX509TrustManager")
- public static class InsecureTrustManager implements X509TrustManager {
- @SuppressLint("TrustAllX509TrustManager")
- @Override
- public void checkClientTrusted(final X509Certificate[] chain, final String authType) {
- }
-
- @SuppressLint("TrustAllX509TrustManager")
- @Override
- public void checkServerTrusted(final X509Certificate[] chain, final String authType) {
- }
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[]{};
- }
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBChromeClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBChromeClient.java
index 9ccd3958dd..9075e35a95 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBChromeClient.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBChromeClient.java
@@ -18,28 +18,27 @@ package nodomain.freeyourgadget.gadgetbridge.webview;
import android.webkit.ConsoleMessage;
import android.webkit.WebChromeClient;
-import android.widget.Toast;
-import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class GBChromeClient extends WebChromeClient {
+ private static final Logger LOG = LoggerFactory.getLogger(GBChromeClient.class);
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) {
- GB.toast(formatConsoleMessage(consoleMessage), Toast.LENGTH_LONG, GB.ERROR);
- //TODO: show error page
+ LOG.error(formatConsoleMessage(consoleMessage));
+ //TODO: show small error indication to user, allowing them to view the error(s)
}
return super.onConsoleMessage(consoleMessage);
}
private static String formatConsoleMessage(ConsoleMessage message) {
String sourceId = message.sourceId();
- if (sourceId == null || sourceId.length() == 0) {
+ if (sourceId == null || sourceId.isEmpty()) {
sourceId = "unknown";
}
return String.format("%s (at %s: %d)", message.message(), sourceId, message.lineNumber());
}
-
-
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java
index e15283f1c6..ca7c531f21 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java
@@ -164,7 +164,7 @@ public class GBWebClient extends WebViewClient {
Uri parsedUri = Uri.parse(url);
if (parsedUri.getScheme().startsWith("http")) {
- if (InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) {
+ if (GBApplication.hasInternetAccess()) {
view.loadUrl(url);
} else {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));